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()を通過う必要があった.



これでこの記事の目標はクリアした.
次は自作の言語にこんな感じのことを組み込みたい.

この記事が気に入ったらサポートをしてみませんか?