Go言語でメロディを奏でる
前回
今回やること
前回はただ単音のシンセサイザを鳴らすだけだったから,もう少しいじってランダムに選ばれたメロディを作る.
環境
・macOS Big Sur ver. 11.5.1(20G80) -> 12.6 (21G115)
・MacBook Air M1, 2020 メモリ 8GB
・go1.18.3 darwin/arm64
ランダムに1音を鳴らす
まずは1音からランダムに鳴らしてみる.
結論から,次のコードで出来た.
webassembly.go
package main
import (
"syscall/js"
"time"
"math/rand"
"math"
)
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 main() {
// グローバルオブジェクト(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)
rand.Seed(time.Now().UnixNano())
randNote := rand.Intn(80)
osc.Get("frequency").Set("value", num_to_freq(randNote))
osc.Set("type", "sine")
osc.Call("connect", ctx.Get("destination"))
osc.Call("start")
body.Call("appendChild", osc)
return nil
}))
// ボタンをbodyに追加します
body.Call("appendChild", btn)
// プログラムが終了しないように待機します
select {}
}
部分ごとに切り分けて少し解説していく.
MIDIノートナンバーから周波数への変換
// 基準となるラの音 (concert A) の周波数
const concert_a_freq = 440;
// 基準となるラの音 (concert A) のMIDIノートナンバー
const concert_a_notenum = 69;
// MIDIノートナンバーを十二平均律で周波数に変換する関数
const convert_to_frequency = (notenum) => {
// 基準音から何音高い/低いかを計算する
const from_concert_a = notenum - concert_a_notenum;
// 周波数を実際に計算する
// 十二平均律では2音の最小の周波数差は`2^(1/12)`となる
const freq = Math.pow(2, from_concert_a / 12) * concert_a_freq;
return freq;
};
JavaScript
⏬
Go
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;
}
concert_a_notenumやconcert_a_freqはグローバル変数のようにしようとしたが,「syntax error: non-declaration statement outside」のようにビルドが失敗してしまうので直接数字を入れた.int,floatの計算もきちんとキャスト変換しておいている.
ランダムなMIDIノートナンバーの選択
ctx := js.Global().Get("AudioContext").New()
osc := js.Global().Get("OscillatorNode").New(ctx)
rand.Seed(time.Now().UnixNano())
randNote := rand.Intn(80)
osc.Get("frequency").Set("value", num_to_freq(randNote))
ここの記事から持ってきた.
randNoteに0〜79までの整数をランダムに取ってきて,それをさっきの周波数に変換する関数に入れている.
音が止まるようにする
作ろうとするものは,ランダムに60個の周波数の音を鳴らしていくものとする.
まずは結果として,作ったコードから
webassembly.go
package main
import (
"syscall/js"
"time"
"math/rand"
"math"
"fmt"
)
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 main() {
// グローバルオブジェクト(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 < 60; 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)
}
fmt.Println("loop exit")
osc.Set("type", "sawtooth")
return nil
}))
// ボタンをbodyに追加します
body.Call("appendChild", btn)
// プログラムが終了しないように待機します
select {}
}
変わったところを見ていこう.
実は今まで不要,というか良くなかったもの
body.Call("appendChild", osc)
これ,消しました.
oscはDOMオブジェクトではないので,これがあるまま実行してボタン押すと,一回は動くけどブラウザのコンソールにpanicって出て,それ以降はリロードしないと動かなくなる.(oscの親はAudioContextに設定されていて特に親子関係を操作する必要はないらしい)
音量を操作する
gain := js.Global().Get("GainNode").New(ctx)
gain.Get("gain").Set("value", 0)
gain.Call("connect", ctx.Get("destination"))
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)
音量を操作するものはgainでできるみたいなので,その周りの値を色々いじってる.
鳴らしたい間だけ音量0.3にしてる.
ctx.Get("currentTime").Float()は,Float()入れないとjs.Valueとfloat64で型が一致しませんって怒られるから注意.
周波数の入れ方
osc.Get("frequency").Call("setValueAtTime", num_to_freq(randNote), ctx.Get("currentTime").Float()+start_t)
前回までは
osc.Get("frequency").Set("value", num_to_freq(randNote))
としていたが,これでやると,最後の1音以外が一瞬で終わってしまい,聞き取れなくなり,60個分の時間を最後の1音だけがなるようになってしまう.
値と鳴らし始める時間の両方を引数できるsetValueAtTime()を通過う必要があった.
これでこの記事の目標はクリアした.
次は自作の言語にこんな感じのことを組み込みたい.
この記事が気に入ったらサポートをしてみませんか?