Golangでカノンを演奏してみた
こんにちは、エンジニアのIkemiです。
この記事は Go Advent Calendar 2021 の10日目の記事です。
今回は、Golangでカノン(パッヘルベル)の一部を演奏してみたいと思います。
step1. sin波を書く
ご存知の通り、音は波ですね。まずはsin波を書いてみましょう。
グラフの出力にはgnuplotを使用しました。
$ gnuplot
$ gnuplot> plot sin(x)
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
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
音楽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)