前回、サイン波を利用して音楽ファイルを生成するプログラムを作りました。前回のプログラムでは、音階を番号で指定しなくてはならず、曲データを入力が大変でした。そこで、前回のプログラムにブラウザから使えるUIをつけて誰でも使えるオルゴールを作ってみましょう。
オルゴールの仕組み
今回作成するのは、ブラウザで曲データの入力ができるようなオルゴールです。ブラウザの画面でオルゴールのピンを指定することで、手軽に曲データを作成できるようにします。
大きな仕組みとしては、Go言語でWebアプリを作成します。アプリの仕組みを図にすると、以下のようになります。
詳しく見てみましょう。まず、Go言語でHTTPサーバーを起動します。ブラウザでサーバーにアクセスすると、オルゴールの画面HTMLを返信します。そして、ユーザーはブラウザ上でオルゴールの曲データを入力します。そして、再生ボタンを押すと、Ajaxで曲データをサーバーに送信し、Goのサーバー側でWAVファイルを生成します。WAVファイルが生成されたら、ブラウザ側でWAVファイルを読み込んで再生します。
なお、WAVファイルの生成部分は前回作成したプログラムをそのまま利用します。そのため、ブラウザ上で作成する曲データは、ピアノの鍵盤を番号で表した番号(ノート番号)をカンマで区切って指定したものとします。この時、0番を指定すると休符を指定したものとみなします。
例えば、「ドレミードレミソ」というデータであれば「60,62,64,0,60,62,64,0」というデータになります。なお、ブラウザの開発者ツールでコンソールを表示すると、どんな曲データを生成したのかを確認できるようにしています。
プロジェクトを作ろう
それでは、前回と同じように、WAVファイルを手軽に生成するために、go-wavというライブラリをインストールしましょう。また、ブラウザでオルゴールを動かすために、WebフレームワークのGinもインストールしましょう。
最初に、コマンドラインで以下のコマンドを実行して、go-wavとGinをインストールしましょう。
$ go mod init example.com/musicbox
$ go get github.com/youpy/go-wav
$ go get github.com/gin-gonic/gin
なお、今回、WAVファイルを生成するサーバー側のGoのプログラム「musicbox.go」と、ピンを置くHTML画面「index.html」と二つのファイルで作ります。以下のようなファイル構成になるようにします。プロジェクト一式はこちらからダウンロードできます。
.
├── go.mod --- Goのモジュール設定ファイル(自動的に作られる)
├── musicbox.go --- オルゴールのプログラム
└── static
├── index.html --- オルゴールの画面
└── box.wav --- WAVファイル(自動的に作られる)
WAVファイル生成サーバーを作ろう
それでは、まずは、Go言語でWAVファイルを生成するHTTPサーバーから見ていきましょう。以下のプログラムを「musicbox.go」という名前で保存しましょう。
package main
import (
"log"
"math"
"net/http"
"os"
"strconv"
"strings"
"github.com/gin-gonic/gin"
"github.com/youpy/go-wav"
)
const sampleRate = 44100.0 // サンプルレートを指定
const bpm = 120 // テンポを指定
func main() {
// サーバーの設定
router := gin.Default()
// オルゴールの画面(HTMLファイル)を返す --- (*1)
router.StaticFS("/static", http.Dir("static"))
router.GET("/", func(ctx *gin.Context) {
ctx.Redirect(302, "/static/index.html")
})
// WAVファイル生成API --- (*2)
router.GET("/make", func(ctx *gin.Context) {
notesStr := ctx.Query("notes")
notes := strings.Split(notesStr, ",")
file, _ := os.Create("static/box.wav")
defer file.Close()
len4 := int(sampleRate * 60 / bpm)
sampleSize := uint32(len(notes) * len4)
writer := wav.NewWriter(file, sampleSize, 1, uint32(sampleRate), 16)
for i := 0; i < len(notes); i++ {
// 各音符を書き込む
no, _ := strconv.Atoi(notes[i])
addNote(writer, no, len4)
}
ctx.String(200, "ok")
})
// サーバーを起動 --- (*3)
err := router.Run("127.0.0.1:8888")
if err != nil {
log.Fatal("サーバー起動に失敗", err)
}
}
// 音符を書き込む --- (*4)
func addNote(writer *wav.Writer, note int, length int) {
// 音階から周波数を計算
tone := 440.0 * math.Pow(2.0, float64(note-69)/12.0)
// サイン波を書き込む
samples := make([]wav.Sample, length)
for i := 0; i < length; i++ {
v := math.Sin((float64(i) / float64(sampleRate)) * tone * 2.0 * math.Pi)
v *= 0.3 // 音量を下げる
// プチッと切れるノイズを軽減
decLen := length / 8 // 音符の最後の音量を下げる
if i > length-decLen {
v *= float64((length - i)) / float64(decLen)
}
samples[i].Values[0] = int(v * 0x7FFF)
}
writer.WriteSamples(samples)
}
簡単にプログラムの説明をします。(*1)の部分では、オルゴールの画面(HTML)が表示されるようにリダイレクトを設定します。また、staticディレクトリ以下に配置したファイルに自由にアクセスできるようにします。
(*2)では「/make」というURLにアクセスした時、WAVファイルが生成されるようにします。URL引数のnotesに指定した曲データを取得し、その曲データに沿ってWAVファイルを生成します。生成が完了したら、okとだけ短くレスポンスを返します。
(*3)ではサーバーを起動します。ここでは、ポート8888でHTTPサーバーを起動します。WebフレームワークのGinを利用しているため、非常に簡潔に各種処理を記述できました。なお、Ginに関しては、本連載の17回目で詳しく解説しているので参考にしてください。
オルゴール画面を作ろう
続いて、オルゴール画面を作成しましょう。staticというフォルダを作成し、以下のHTMLを「index.html」という名前で保存します。オルゴールのピンは、HTMLのラジオボックスをそのまま利用しています。
<html><meta charset="utf-8">
<body>
<h1>オルゴール<button onclick="play()">再生</button></h1>
<div id="box"></div>
</body>
<script>
const MAX_ROWS = 32
// オルゴール画面を作成(ラジオボックスをたくさん作成) --- (*1)
const box = document.getElementById('box')
const colors = [0,1,0,1,0,0,1,0,1,0,1,0]
let html = ''
for (let row = 0; row < MAX_ROWS; row++) {
let cols = ''
for (let col = 0; col < 24; col++) {
let bg = (colors[col % 12]) ? 'background-color:#CCC;' : ''
switch (row % 8) {
case 3: bg += `border-bottom:2px solid silver;`; break;
case 7: bg += `border-bottom:2px solid gray;`; break;
}
const va = 60 + col
cols += `<td style="${bg}border-left:1px solid #999;">`
cols += `<input type="radio" id="g${row}" name="g${row}" value="${va}">`
cols += `</td>`
}
html += `<tr>${cols}</tr>`
}
box.innerHTML = `<form id="boxform"><table>${html}</table></form>`
// 再生ボタンを押した時の処理
function play() {
// オルゴールデータを作成 --- (*2)
const res = []
const form = document.getElementById('boxform')
for (let i = 0; i < MAX_ROWS; i++) {
let v = form[`g${i}`].value
if (!v) v = 0
res.push(v)
}
const s = res.join(',').replace(/(,0)+$/, '')
console.log('notes=', s)
// サーバーに送信 --- (*3)
fetch('/make?notes=' + s).then(r => r.text()).then((text) => {
console.log('response=', text)
const audio = new Audio('/static/box.wav?r=' + s)
audio.currentTime = 0
audio.play()
})
}
</script>
</html>
簡単にHTML/JavaScriptについて紹介します。オルゴールのピンはラジオボックスをたくさん表示して実現していますが、(*1)の部分でHTMLをJavaScriptによって動的に生成しています。
そして、再生ボタンを押した時、(*2)ラジオボックスからピンの刺された部分の曲データを取り出します。それから、(*3)でfetch APIを利用してサーバーに曲データを送信します。レスポンスが返ってきたら、WAVファイルを再生するようにします。
オルゴールを実行しよう
それでは、プログラムを実行してみましょう。プログラムを実行するには、コマンドラインで以下のコマンドを実行します。
$ go run .
すると、HTTPサーバーが起動するので、ブラウザで「http://localhost:8888」にアクセスします。するとオルゴールの画面が表示されます。横方向が音程で、縦方向が時間となっています。音程は左から、ドド#レレ#ミファソ・・・とピアノの鍵盤のように並んでいます。ピンを刺したら画面上部の「再生」ボタンを押します。すると、WAVファイルが生成され、音楽が再生されます。
まとめ
以上、今回はオルゴールのプログラムを作ってみました。特に、前回作成したWAVファイルの生成プログラムをちょっと改良して、HTML/JavaScriptで画面を作ってみました。GoのプログラムとHTMLファイルを足しても115行しかありません。見通しが良いので、この手のプログラムを作る参考になることでしょう。
ただし、実行してみると分かりますが、オルゴールと呼ぶにはあまりにも再生される音が素朴ですよね。WAVファイルを生成する処理を改良して、より良い音が鳴るようにいろいろ改良してみると良いでしょう。実際に音が鳴る音楽プログラミングは面白いので、ぜひ挑戦してみてください。
自由型プログラマー。くじらはんどにて、プログラミングの楽しさを伝える活動をしている。代表作に、日本語プログラミング言語「なでしこ」 、テキスト音楽「サクラ」など。2001年オンラインソフト大賞入賞、2004年度未踏ユース スーパークリエータ認定、2010年 OSS貢献者章受賞。技術書も多く執筆している。直近では、「シゴトがはかどる Python自動処理の教科書(マイナビ出版)」「すぐに使える!業務で実践できる! PythonによるAI・機械学習・深層学習アプリのつくり方 TensorFlow2対応(ソシム)」「マンガでざっくり学ぶPython(マイナビ出版)」など。