見出し画像

Golangでカノンを演奏してみた

こんにちは、エンジニアのIkemiです。

この記事は Go Advent Calendar 2021 の10日目の記事です。

今回は、Golangでカノン(パッヘルベル)の一部を演奏してみたいと思います。

step1. sin波を書く

ご存知の通り、音は波ですね。まずはsin波を書いてみましょう。
グラフの出力にはgnuplotを使用しました。

$ gnuplot
$ gnuplot> plot sin(x)

画像1

Golangで書くとこうなります。
(All code: https://github.com/mc-ikemiryo/generate_canon/blob/main/step1/main.go)

const samples = 1000
const tau = 2 * math.Pi
const rad = tau / samples
for i := 0; i < samples; i++ {
	samp := math.Sin(rad * float64(i))
	fmt.Printf("%.10f\n", samp)
}
$ go run main.go > sin.txt
$ gnuplot
$ gnuplot> plot "sin.txt" with lines

画像2

step2. 音声ファイル生成

まずは、音声ファイル生成に必要な項目を確認しましょう。

サンプリング周波数 [Hz] or [1/s]
ここでは1秒間あたりのサンプル(plot)の数です。
サンプル数を50にしてsin波を書いてみましょう。

const samples = 50
const tau = 2 * math.Pi
const rad = tau / samples
for i := 0; i < samples; i++ {
	samp := math.Sin(rad * float64(i))
	fmt.Printf("%.10f\n", samp)
}
$ go run main.go > sin.txt
$ plot "sin.txt" with linespoints pointtype 7

画像3

音楽CDで使用されるサンプリング周波数は44.1kHzなので、今回はそれと同じにします。

サンプル数 [-]
出力する音のサンプル数です。サンプリング周波数×秒数で表されます。

振動数 [Hz] or [1/s]
簡単にいうと、単位時間あたりの波の数ですね(物理学的な定義の説明はここではしません)。

バイト順序
リトルエンディアンかビッグエンディアンを指定します。音声ファイルを読み込むときに正しく指定しないと処理できません。

上記をもとに5秒間の音声を生成してみましょう。
周波数は440Hzにしました。
音声出力にはffplayを使用しました。

(All code: https://github.com/mc-ikemiryo/generate_canon/blob/main/step2/main.go)

const (
	soundLength   = 5
	samplesPerSec = 44100
	tau           = 2 * math.Pi
	frequency     = 440
)

func main() {
	file, _ := os.Create("out.bin")

	samples := samplesPerSec * soundLength
	for i := 0; i < samples; i++ {
		sample := math.Sin((tau * frequency * float64(i)) / samplesPerSec)
		buf := make([]byte, 4)

		// バイト順序=LittleEndian
		binary.LittleEndian.PutUint32(buf, math.Float32bits(float32(sample)))
		file.Write(buf)
	}
}
$ go run main.go
$ ffplay -i out.bin -f f32le -showmode 1

出力した音声はこちらです。

以下のサイトの 49番の 440Hz の音(ラの音)に一致しますね。

step3. 音を減衰させる

音が機械的で味気ないので、自然な感じに減衰させましょう。

ここではシンプルに、sample数ごとにx倍していきましょう。
例えばsample数が100の時、最後の音量(振幅)が最初(大きさ1)の1/100000倍になるよう減衰させるとき、100回x倍すると1/100000になるので、

1*x^100 = 1*1/100000
x = 1/100000^(1/100)

となりますね。
これをコードで表すと、
(All code: https://github.com/mc-ikemiryo/generate_canon/blob/main/step3/main.go)

const (
	soundLength   = 5
	samplesPerSec = 44100
	tau           = 2 * math.Pi
	frequency     = 440

	end = 1.0e-5
)

func main() {
	file, _ := os.Create("out.bin")

	samples := samplesPerSec * soundLength
	damping := math.Pow(end, 1.0/float64(samples))
	for i := 0; i < samples; i++ {
		sample := math.Sin((tau * frequency * float64(i)) / samplesPerSec)
		sample = sample * math.Pow(damping, float64(i))
		buf := make([]byte, 4)

		// バイト順序=LittleEndian
		binary.LittleEndian.PutUint32(buf, math.Float32bits(float32(sample)))
		file.Write(buf)
	}
}
​
$ go run main.go
$ ffplay -i out.bin -f f32le -showmode 1

出力した音声はこちらです。

step4. 音階を作る

さて、今度は音階をつけてみましょう。
先程のサイトを参考にC4-C5 (ドレミファソラシド)を実装してみます。
(end = 1.0e-2 に変更しています)

(All code: https://github.com/mc-ikemiryo/generate_canon/blob/main/step4/main.go)

const (
	soundLength   = 1
	samplesPerSec = 44100
	tau           = 2 * math.Pi

	end = 1.0e-2

	C4 = 261.626
	D4 = 293.665
	E4 = 329.628
	F4 = 349.228
	G4 = 391.995
	A4 = 440.000
	B4 = 493.883
	C5 = 523.251
)

func main() {
	file := "out.bin"
	f, _ := os.Create(file)
	generate(C4, f)
	generate(D4, f)
	generate(E4, f)
	generate(F4, f)
	generate(G4, f)
	generate(A4, f)
	generate(B4, f)
	generate(C5, f)
}

func generate(frequency float64, file *os.File) {
	samples := samplesPerSec * soundLength
	damping := math.Pow(end, 1.0/float64(samples))
	for i := 0; i < samples; i++ {
		sample := math.Sin((tau * frequency * float64(i)) / samplesPerSec)
		sample = sample * math.Pow(damping, float64(i))
		buf := make([]byte, 4)

		// バイト順序=LittleEndian
		binary.LittleEndian.PutUint32(buf, math.Float32bits(float32(sample)))
		file.Write(buf)
	}
}
$ go run main.go
$ ffplay -i out.bin -f f32le -showmode 1

出力した音声はこちらです。
ドレミフアソラシドになってますね。

step5. メロディを作る

音階の作り方が分かったので、今度はメロディを作りましょう。
カノンの楽譜はこちらです。
https://store.piascore.com/scores/82533
1秒=4分の4拍子とします。
(end = 1.0e-1 に変更しています)

(All code: https://github.com/mc-ikemiryo/generate_canon/blob/main/step5/main.go)

const (
	samplesPerSec int = 44100
	tau               = 2 * math.Pi

	end = 1.0e-1

	C3  = 130.813
	D3  = 146.832
	# 以下省略
)

func main() {
	file := "out.bin"
	f, _ := os.Create(file)
	generate(A5, 2, f)
	generate(Fs5, 1, f)
	generate(G5, 1, f)
	# 以下省略
}

func generate(frequency float64, soundLength int, file *os.File) {
	samples := (soundLength * samplesPerSec) / 4
	damping := math.Pow(end, 1.0/float64(samples))
	for i := 0; i < samples; i++ {
		sample := math.Sin((tau * frequency * float64(i)) / float64(samplesPerSec))
		sample = sample * math.Pow(damping, float64(i))
		buf := make([]byte, 4)

		// バイト順序=LittleEndian
		binary.LittleEndian.PutUint32(buf, math.Float32bits(float32(sample)))
		file.Write(buf)
	}
}
$ go run main.go
$ ffplay -i out.bin -f f32le -showmode 1

出力した音声はこちらです。

カノンの例のあの部分になってますね。

step6. コード進行を作る

次はコード進行を実装しましょう。

コードは3つ以上の音を重ね合わせで、和音とも言います。
なので、3つの音をそれぞれ0.333倍したものを、足し合わせればOKですね。(有効桁数については主題から外れるので言及しません)

カノンのコードは、D - A - Bm - F#m - G - D - G - A、です。
(end = 1.0e-2 に変更しています)

(All code: https://github.com/mc-ikemiryo/generate_canon/blob/main/step6/main.go)

const (
	soundLength   = 1
	samplesPerSec = 44100
	tau           = 2 * math.Pi

	end = 1.0e-2

	C3  = 130.813
	D3  = 146.832
	// 以下略
)

func main() {
	file := "out.bin"
	f, _ := os.Create(file)
	generate(A3, D4, Fs4, f)
	generate(A3, D4, Fs4, f)
	// 以下略
}

func generate(frequency1, frequency2, frequency3 float64, file *os.File) {
	samples := samplesPerSec * soundLength
	damping := math.Pow(end, 1.0/float64(samples))
	for i := 0; i < samples; i++ {
		sample := 0.333*math.Sin((tau*frequency1*float64(i))/samplesPerSec) +
			0.333*math.Sin((tau*frequency2*float64(i))/samplesPerSec) +
			0.333*math.Sin((tau*frequency3*float64(i))/samplesPerSec)

		sample = sample * math.Pow(damping, float64(i))
		buf := make([]byte, 4)

		// バイト順序=LittleEndian
		binary.LittleEndian.PutUint32(buf, math.Float32bits(float32(sample)))
		file.Write(buf)
	}
}
$ go run main.go
$ ffplay -i out.bin -f f32le -showmode 1

出力した音声はこちらです。

step7. メロディとコードの合成

最後に、メロディとコードを合成して、曲を完成させましょう。
それぞれのbinaryファイルを一度float32に復元して、それぞれを0.5倍して足し合わせましょう。

(All code: https://github.com/mc-ikemiryo/generate_canon/blob/main/step7/main.go)

melodyFile, _ := os.Open("./melody.bin")
defer melodyFile.Close()
melodyF := []float32{}
for {
	b := make([]byte, 4)
	_, err := melodyFile.Read(b)
	if err == io.EOF {
		break
	}
	u := binary.LittleEndian.Uint32(b)
	f := math.Float32frombits(u)
	melodyF = append(melodyF, f)
}

codeFile, _ := os.Open("./code.bin")
defer codeFile.Close()
codeF := []float32{}
for {
	b := make([]byte, 4)
	_, err := codeFile.Read(b)
	if err == io.EOF {
		break
	}
	u := binary.LittleEndian.Uint32(b)
	f := math.Float32frombits(u)
	codeF = append(codeF, f)
}

file := "out.bin"
f, _ := os.Create(file)
samples := 705600
for i := 0; i < samples; i++ {
	sample := 0.5*melodyF[i] + 0.5*codeF[i]
	buf := make([]byte, 4)

	// バイト順序=LittleEndian
	binary.LittleEndian.PutUint32(buf, math.Float32bits(float32(sample)))
	f.Write(buf)
}
$ go run main.go
$ ffplay -i out.bin -f f32le -showmode 1

完成した音楽はこちらです。

かなりいい感じになりましたね。

さらに、ヘッダーを追加して、WAVファイル形式にしてもよいかもです。

最後に

homieでは一緒に働く仲間を絶賛募集中です。興味をお持ちの方はお問い合わせください。

参考

中高生・学部低回生向け実験 by 立命館大学 理工学部 電子情報工学科 いずみ研 音と波とコンピュータ~WAVファイル生成~
http://www.ritsumei.ac.jp/se/re/izumilab/lecture/17pcsound/

gnuplot
http://www.gnuplot.info/

ffplay
https://ffmpeg.org/ffplay.html

macOSにhomebrewでffplayをインストールする
https://qiita.com/miminashi/items/50ce8f001bb7d3bbb15f

音階の周波数
https://tomari.org/main/java/oto.html

カノン楽譜
https://store.piascore.com/scores/82533

カノン (パッヘルベル) wiki
https://ja.wikipedia.org/wiki/%E3%82%AB%E3%83%8E%E3%83%B3_(%E3%83%91%E3%83%83%E3%83%98%E3%83%AB%E3%83%99%E3%83%AB)

この記事が参加している募集