生成AIと一緒に学ぶ、Phel製 MML→WAV 変換ツールの制作記

音の仕組みを知りたい、という思いつきから
ギターを弾くのが趣味ではあるので周波数によって音程が変わるくらいの知識はありますが、「音を操作するプログラムってどうやって作るんだろ?」という素朴な疑問が浮かびました。音楽を作ることを想定している訳ではないけれど、プログラミングで音を扱ってみたくなったのです。といっても、音の仕組みはまったくわかっていませんでした。
そんなときに頼ったのが、ChatGPTでした。
Pythonで音を鳴らすプログラムを見るところからスタート
まずは手軽な方法を知りたかったので、「PHPで音を鳴らせないか?」と相談してみました。けれど、PHP単体でのリアルタイムな音の出力は難しそうだとわかり、Pythonを勧められました。
Pythonでは numpy
を使って波形データを生成し、sounddevice
や scipy.io.wavfile.write
を使えば簡単に音を鳴らしたりWAVファイルを書き出せるということを教わりました。Pythonで書かれたコードは凄く単純なコードで、何が行なわれているのか理解できなかったので、ChatGPTに段階的に細かい部分にフォーカスして説明してもらいました。
ここで学んだことを簡単にまとめてみます。
サイン波とは?
音は空気の振動であり、特定の周波数で振動すると「音程」として耳に届きます。この振動を最も単純な形で表すと、サイン波(正弦波)になります。
Pythonでは numpy.sin()
を使ってこの波を生成できます。例えば、440Hz(A=ラ)の音を1秒間鳴らしたい場合、以下のようなコードになります:
import numpy as np
rate = 44100 # 1秒間にサンプリングする回数
t = np.linspace(0, 1, int(rate)) # 時間の配列を生成
freq = 440
wave = 0.5 * np.sin(2 * np.pi * freq * t)
このコードで得られる wave
配列は、時間に対してどのくらい振幅があるか(つまり空気の揺れ具合)を数値で表したものです。
最初にコードを見た時には、t
が配列であることもわからず、事細かにChatGPTに教えてもらいました。numpy
では、配列に対しての乗算は配列の各要素に対しての乗算になるので、このようなシンプルな記述になっているということがわかりました。(numpy
に対する知識がないので厳密には語弊があるかもしれません)
PCMとWAVフォーマットの基本
音声データをWAVファイルとして保存するには、いくつかの工程が必要です。
- 波形のスケーリング: サイン波の値は -1.0〜+1.0 の範囲ですが、WAVファイルでは通常16bitの整数(-32768〜32767)で表します。よって次のようにスケーリングします:
pcm_data = np.int16(wave * 32767)
- WAVファイルの書き出し: Pythonでは
scipy.io.wavfile.write
を使って簡単に出力できます。from scipy.io.wavfile import write write("output.wav", rate, pcm_data)
さらに、ここから一歩踏み込んで「自分でWAVファイルのバイナリを書き出す」方法も教えてもらいました。Pythonのwave
モジュールやstruct
モジュールを使って、WAVフォーマットのヘッダ構造(RIFFチャンク、fmtチャンク、dataチャンクなど)を学び、バイト列を手動で組み立ててWAVを出力する過程は、音声ファイルの内部構造への理解を深める大きなステップとなりました。
こうして音の仕組みの輪郭が見えてきました。
Phelでツールを作ってみたくなる
音の仕組みが少しずつ理解できてきたので、「これをPythonじゃなくて Phelで書けるんじゃないか?」という興味が湧いてきました。
実際に、GitHub Copilotに手伝ってもらいながら、実装してみました。 Phelは情報が多いとは言えない言語なので GitHub Copilot(Claude 3.7 Sonnet)でもコード生成は難しいのでは…と半信半疑でしたが、驚いたことにかなり有用なコードを提案してくれました。時には類似のシンタックスを持つClojureにしか無い関数が混ざったり、関数型らしくないコードとかも生成されましたが、コードについては、関数を丸ごと書き換えたりして動くようにしました。
作ったもの: MML → WAV 変換ツール
今回作成したツールは、Music Macro Language (MML) の文字列を標準入力から受け取り、それに応じた音波を合成してWAVファイルとして標準出力に出力するプログラムです。
特徴:
- MML記法:
t120 o5 c d e f g a b > c
のような簡易的な音楽記述 - sin波による音波生成
- 16bit PCM形式でのWAV出力
- 標準入力/出力対応でCLIツールとして活用可能
GitHub Copilotとの共同作業で気づいたこと
- 非主流言語でも頑張ってくれる: Phelのようなメジャーとは言えない言語でも、Clojureと勘違いしたコードを出力することもありましたが、予想以上に有効な提案を出してくれました。
- 関数型らしいコードを生成するのは苦手: 基本的にほぼミュータブルな変数操作や命令的な書き方をします。命令的な書き方は、だいたい動かなかったので関数型のスタイルに書き直す必要がありました。
- コンセプトを共有すると強い: 「最終的に16bit PCMのバイナリをWAVとして出力したい」「標準出力に出したい」といった要件を明確に伝えることで、驚くほどスムーズに話が進みました。
- Askモードで仕様検討の調査や相談をした後に指示書を出力してもらい、Editモードで指示書を元に作業してもらうというのが良い流れに感じています。(Agentモードはまだ使ってないです)
まとめ
今回のプロジェクトは、音の仕組みを学ぶという好奇心から始まり、ChatGPTやGitHub Copilot(Claude 3.7 Sonnet)の力を借りて形になったものです。 AIとの協働によって、未知の領域に気軽に飛び込める感覚を味わえたのが、とても楽しかったです。 「音をプログラムしてみたい」「ちょっと変わった言語で遊んでみたい」と思っている方は、ぜひ気軽にcloneして触ってみてください。