WebAudioAPIを言語に組み込む際に困ったこと
main.goにサーバーとしてのコードを付け加える.
main.go
package main
import(
"fmt"
"os"
"os/user"
"monkey/repl"
"log"
"net/http"
)
func main(){
port := "8080"
http.Handle("/", http.FileServer(http.Dir("./docs/")))
log.Printf("Listen on port: %s", port)
log.Fatal(http.ListenAndServe(":"+port, nil))
user,err := user.Current()
if err != nil{
panic(err)
}
fmt.Printf("Hello %s. This is The Monkey Programming language.\n", user.Username)
fmt.Printf("Feel free to type in commands\n")
repl.Start(os.Stdin, os.Stdout)
}
copy
まず,この状態で前回の記事と同じようにビルド,main.goの実行をすることで期待通りWebAudioAPIが使える状態か確認する.
結論から言えば,音は鳴った.ただし,命令分を実行させることができなくなってしまっていた.
サーバーとしての役割は切り離した方が良さそうだ.
元のmain.goに戻す.
同一ディレクトリにmain関数が複数あるとエラーが発生するので,別の箇所にserver.goとして以下のコードを置いておく必要がありそうだ.
server/server.go
package main
import (
"log"
"net/http"
)
func main() {
port := "8080"
http.Handle("/", http.FileServer(http.Dir("../docs/")))
log.Printf("Listen on port: %s", port)
log.Fatal(http.ListenAndServe(":"+port, nil))
}
copy
http.Handleはディレクトリ構成の点から"./docs/"から"../docs/"に変更している.
一度ディレクトリ構成を整理しよう,
monkeyを作った時のものと変更がないディレクトリについては中身は省略する.
├── ast
├── docs
│ ├── build.wasm
│ ├── index.html
│ └── wasm_exec.js
├── evaluator
├── lexer
├── object
├── parser
├── repl
├── server
│ └── server.go
├── token
├── wasm
│ └── webassembly.go
├── go.mod
├── go.sum
└── main.go
copy
複数ファイルの実行
サーバーの処理を分けると,main.goとこのserver.goを同時に実行しなければいけないはず.
ただ,実行してみると
MacBook-Air halo % go run main.go server/server.go
named files must all be in one directory; have . and server
copy
一つのディレクトリにないと同時に実行できないらしい……
かといって同じディレクトリに置くとさっきのエラーの問題がでてくるし
ターミナルを分けてやってみたら動くみたい.
ブラウザのテキストボックスに入力
このサイトを見てみると,引数はブラウザのテキストボックスに入れて指定することができるっぽい,
やりたいこととは少し違うけど,使えるかもしれないからちょっと見てみる
失敗してごちゃごちゃになると面倒だから一旦テスト環境に移動する.
テスト環境はこの記事をやったところにしてる
index.html
<html>
<head>
<meta charset="utf-8"/>
<script src="wasm_exec.js"></script>
<script>
const go = new Go();
WebAssembly.instantiateStreaming(fetch("build.wasm"), go.importObject).then((result) => {
go.run(result.instance);
});
</script>
</head>
<body>
<input type="text" id="noteNum" />
<button onClick="play('noteNum');" id="playButton">play</button>
</body>
</html>
<body>のところに新しくテキストボックスとplayボタンを追加した
webassembly.go
package main
import (
"syscall/js"
"time"
"math/rand"
"math"
"fmt"
"strconv"
)
func num_to_freq (notenum int) float64{
// 基準音から何音高い/低いかを計算する
from_concert_a := notenum - 69
// 周波数を実際に計算する
// 十二平均律では2音の最小の周波数差は`2^(1/12)`となる
freq := math.Pow(2, float64(from_concert_a) / 12) * 440;
return freq;
}
func registerCallbacks() {
js.Global().Set("play", js.FuncOf(play))
}
func textToStr(v js.Value) string {
return js.Global().Get("document").Call("getElementById", v.String()).Get("value").String()
}
func play(this js.Value, args []js.Value) interface{} {
value := textToStr(args[0])
noteNum, _ := strconv.Atoi(value)
ctx := js.Global().Get("AudioContext").New()
osc := js.Global().Get("OscillatorNode").New(ctx)
gain := js.Global().Get("GainNode").New(ctx)
gain.Get("gain").Set("value", 0)
bpm := 120.0
note_length := 60.0 / bpm
osc.Call("connect", gain)
gain.Call("connect", ctx.Get("destination"))
osc.Call("start")
for n := 0; n < 60; n++{
start_t := float64(n) * note_length
end_t := start_t + 1.0
osc.Get("frequency").Call("setValueAtTime", num_to_freq(noteNum), ctx.Get("currentTime").Float()+start_t)
gain.Get("gain").Call("setValueAtTime", 0.3, ctx.Get("currentTime").Float()+start_t)
gain.Get("gain").Call("setValueAtTime", 0., ctx.Get("currentTime").Float()+end_t)
}
osc.Set("type", "sawtooth")
return nil
}
func main() {
registerCallbacks()
// グローバルオブジェクト(window)を取得します
window := js.Global()
// document オブジェクトを取得します
document := window.Get("document")
// bodyを取得します
body := document.Get("body")
// ボタンのDOMを作成し、Clickイベントを設定します
btn := document.Call("createElement", "button")
btn.Set("textContent", "music start!")
btn.Call("addEventListener", "click", js.FuncOf(func(js.Value, []js.Value) interface{} {
ctx := js.Global().Get("AudioContext").New()
osc := js.Global().Get("OscillatorNode").New(ctx)
gain := js.Global().Get("GainNode").New(ctx)
gain.Get("gain").Set("value", 0)
bpm := 120.0
note_length := 60.0 / bpm
osc.Call("connect", gain)
gain.Call("connect", ctx.Get("destination"))
osc.Call("start")
rand.Seed(time.Now().UnixNano())
for n := 0; n < 120; n++{
randNote := rand.Intn(20) + 55
fmt.Println(randNote)
start_t := float64(n) * note_length
end_t := start_t + 1.0
osc.Get("frequency").Call("setValueAtTime", num_to_freq(randNote), ctx.Get("currentTime").Float()+start_t)
gain.Get("gain").Call("setValueAtTime", 0.3, ctx.Get("currentTime").Float()+start_t)
gain.Get("gain").Call("setValueAtTime", 0., ctx.Get("currentTime").Float()+end_t)
}
osc.Set("type", "sawtooth")
return nil
}))
// ボタンをbodyに追加します
body.Call("appendChild", btn)
// プログラムが終了しないように待機します
select {}
}
JSスクリプトエンジン
「JSスクリプトエンジン」を使ってシナリオプログラムを記述しそれに基づいて音楽を鳴らす最小のサンプルを手に入れたので以下に記す,これをうまいこと組み込めれば目標が実現できるかもしれない..
package main
import (
"math"
"syscall/js"
)
var (
window = js.Global()
document = window.Get("document")
AudioContext = js.Global().Get("AudioContext")
OscillatorNode = js.Global().Get("OscillatorNode")
GainNode = js.Global().Get("GainNode")
note2freq = []float64{}
)
func init() {
for i := 0; i < 128; i++ {
note2freq = append(note2freq, 440*math.Pow(2, float64(i-69)/12))
}
}
type Note struct {
Number int
Duration float64
}
type MusicBox struct {
ctx js.Value
osc js.Value
gain js.Value
current float64
}
func NewMusicBox() *MusicBox {
ctx := AudioContext.New()
osc := OscillatorNode.New(ctx)
gain := GainNode.New(ctx)
osc.Call("connect", gain)
osc.Get("frequency").Set("value", 0)
gain.Call("connect", ctx.Get("destination"))
gain.Get("gain").Set("value", 0)
osc.Call("start")
return &MusicBox{
ctx: ctx,
osc: osc,
gain: gain,
current: ctx.Get("currentTime").Float(),
}
}
func (mb *MusicBox) Play(note Note) {
mb.osc.Get("frequency").Call("setValueAtTime", note2freq[note.Number], mb.current)
mb.gain.Get("gain").Call("setValueAtTime", 0.3, mb.current)
mb.gain.Get("gain").Call("setValueAtTime", 0.0, mb.current+note.Duration)
mb.current += note.Duration
}
const sample = `for (let i=0; i< 3; i++) {
play(64, 0.2)
play(0, 0.1)
play(62, 0.2)
play(0, 0.1)
play(60, 0.2)
play(0, 0.1)
}
`
func main() {
code := document.Call("createElement", "textarea")
code.Set("id", "code")
code.Set("value", sample)
code.Get("style").Set("width", "50%")
code.Get("style").Set("height", "20em")
document.Get("body").Call("appendChild", code)
btn := document.Call("createElement", "button")
btn.Set("textContent", "music start!")
btn.Call("addEventListener", "click", js.FuncOf(func(js.Value, []js.Value) interface{} {
mb := NewMusicBox()
window.Set("play", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
mb.Play(Note{args[0].Int(), args[1].Float()})
return nil
}))
code := document.Call("getElementById", "code")
src := code.Get("value")
window.Call("eval", src)
return nil
}))
document.Get("body").Call("appendChild", btn)
select {}
}
動作例
このコードの部分ごとに分けて見ていこうと思う.
var (
window = js.Global()
document = window.Get("document")
AudioContext = js.Global().Get("AudioContext")
OscillatorNode = js.Global().Get("OscillatorNode")
GainNode = js.Global().Get("GainNode")
note2freq = []float64{}
)
"var"は":="の代わりであり,ここは変数の定義,および初期化をしている,上から5つまではこれまで使ってきたものと同じである.最後のはfloat64型のスライスとなっていて,Note to Frequencyのことを指していると考えられる.
func init() {
for i := 0; i < 128; i++ {
note2freq = append(note2freq, 440*math.Pow(2, float64(i-69)/12))
}
}
自分の記事で作ったコードでは毎回MIDIノートナンバーに対して変換を行っていたが,ここでは0〜127のノートナンバーに対してあらかじめ周波数を対応づけてスライスに格納してるようだ.
type Note struct {
Number int
Duration float64
}
MIDIノートナンバーと音長を持った構造体を宣言している.
type MusicBox struct {
ctx js.Value
osc js.Value
gain js.Value
current float64
}
音量や時間,振動子を制御するためのものを構造体でまとめている
func NewMusicBox() *MusicBox {
ctx := AudioContext.New()
osc := OscillatorNode.New(ctx)
gain := GainNode.New(ctx)
osc.Call("connect", gain)
osc.Get("frequency").Set("value", 0)
gain.Call("connect", ctx.Get("destination"))
gain.Get("gain").Set("value", 0)
osc.Call("start")
return &MusicBox{
ctx: ctx,
osc: osc,
gain: gain,
current: ctx.Get("currentTime").Float(),
}
}
MusicBoxの中身のインスタンスを作成してMusicBoxを返す関数
func (mb *MusicBox) Play(note Note) {
mb.osc.Get("frequency").Call("setValueAtTime", note2freq[note.Number], mb.current)
mb.gain.Get("gain").Call("setValueAtTime", 0.3, mb.current)
mb.gain.Get("gain").Call("setValueAtTime", 0.0, mb.current+note.Duration)
mb.current += note.Duration
}
MusicBoxの下にPlay関数を定義している.Noteを引数としていて,そのNumberで周波数を,Durationで音の長さを決定させている.
音の大きさは,鳴らすときは一律で0.3にしている.
const sample = `for (let i=0; i< 3; i++) {
play(64, 0.2)
play(0, 0.1)
play(62, 0.2)
play(0, 0.1)
play(60, 0.2)
play(0, 0.1)
}
`
func main() {
code := document.Call("createElement", "textarea")
code.Set("id", "code")
code.Set("value", sample)
code.Get("style").Set("width", "50%")
code.Get("style").Set("height", "20em")
document.Get("body").Call("appendChild", code)
btn := document.Call("createElement", "button")
btn.Set("textContent", "music start!")
btn.Call("addEventListener", "click", js.FuncOf(func(js.Value, []js.Value) interface{} {
mb := NewMusicBox()
window.Set("play", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
mb.Play(Note{args[0].Int(), args[1].Float()})
return nil
}))
code := document.Call("getElementById", "code")
src := code.Get("value")
window.Call("eval", src)
return nil
}))
document.Get("body").Call("appendChild", btn)
select {}
}
code:= 〜 document.Get()
まででは,コードを記述できるテキストボックスについて設定している.テキストボックスの幅や,高さを決定しています."em"を高さに使っているので,コード行に応じて伸縮するようにはなっていないと思う.後で確認する.
これ以降はボタンの作成,押した後の動作を記述している.
mbのMusicBoxのインスタンスを作成し,Play関数にIntで第一引数,Floatで第二引数を入れている.
紛らわしいですが,上でidが"code"になっているのはcodeなので,それを抽出しようとしています.
わざわざ
code := document.Call("getElementById", "code")
のようにしているのは,セットアップとイベントの発火処理を別々に書くためのようです.
作り込んでいくようになると,このようにするのが良いらしいです.
次にあらかじめ設定されたsampleのコードが入ってるvalueをsrcとし,それをwindow.evalで評価して結果を得ています.
実際に動かしてわかったこと
・constでコードに入れていたものがブラウザ上では最初から買いてあるが,それに加えてplay()の命令を加えても問題なく動く,
・逆にconstになっている部分をブラウザ上で消しても問題ない.
・const sample = ``のようにwebassembly上で空にしておけば真っ白なテキストボックスの状態で出てきて,そこに命令を書いても正しく動く.
これを使って組み込むには
テキストボックスに命令をかけるようにし,ボタンを押すとその文字列をMonkeym言語のreplに入力として渡せればよいのではないか.
長くなってきたので,これについて考える記事は以下に託すことにする.