SoundMakerでチップチューン音声パイプラインの作り方
2026-05-31
はじめに
チップチューン音楽を作成できるWebアプリ「PICOM」では、チップチューンの音を自前で合成しています。その心臓部で使っているのが、私が以前から作っている自作OSSのSoundMakerです。
この記事では、楽譜データ(音符・休符・連符など)からPCM波形を生成し、最終的にWAVバイナリに落とすまでのパイプラインを紹介します。SoundMakerのドッグフーディングの話は以前書きましたが、今回は「実装そのもの」にフォーカスします。
SoundMakerを使って楽譜データからPCM波形を合成する実装を紹介します。FormatBuilderでサウンドフォーマットを構築し、TrackBaseSoundにトラックを載せ、バッファリング方式で1秒ごとにPCMチャンクを取り出してWAVバイナリに結合します。プレビュー用の単音はキャッシュすることで、ユーザーが音符を置くたびに再計算しない工夫も解説します。
パイプラインの全体像
まずは全体の流れです。
FormatBuilderでサウンドフォーマット(サンプリングレート・ビット深度・チャンネル数)を構築TrackBaseSoundを作り、各トラックの波形タイプ(矩形波・三角波・ノイズなど)を設定- 楽譜モデル(PICOMの
Note/Rest/Tie/Tuplet)をSoundMakerの音符オブジェクトに変換 - サンプル位置単位でPolyphonicTrackに音符を追加
GenerateBufferedStereoWaveでPCMチャンクをストリーム生成- チャンクを連結してWAVファイルにラップ
この流れをコードで見ていきます。
FormatBuilder:サウンドフォーマットの宣言
SoundMakerはビルダパターンでフォーマットを記述できます。PICOMは標準の44.1kHz / 16bit / ステレオで固定しています。
private static FormatBuilder CreateFormatBuilder()
{
return FormatBuilder.Create()
.WithFrequency(44100)
.WithBitDepth(16)
.WithChannelCount(2);
}
ゆくゆくはこのパラメータもユーザ側で設定できるようにしようと考えています。
サンプル位置計算:128分音符単位→サンプル単位
楽譜エディタ側では音符の位置を「128分音符何個目か」という整数で管理しています。これをSoundMakerに渡す時に、サンプル数単位に変換する必要があります。
// 128分音符1つ分のサンプル数
var samplesPer128Note = (int)((int)soundFormat.SamplingFrequency * 60 / (tempo * 32d));
foreach (var (position, component) in track.GetAllComponents())
{
var smComponent = component.ToSoundMakerComponent();
SetVolumeTo(smComponent, track.Volume);
var samplePosition = samplesPer128Note * position;
polyphonicTrack.AddAt(samplePosition, smComponent);
}
式の導出を簡単に。4分音符は1分間にtempo回ある(テンポの定義そのもの)ので、1つ分の時間は60 / tempo秒。128分音符はその1/32なので60 / (tempo * 32)秒。サンプル数はsamplingFrequency * 時間なので、掛け算すると上の式になります。
たとえばtempo=120、44100HzならsamplesPer128Note ≈ 689。つまり128分音符1つは約689サンプル、という数字で扱います。
ここで浮動小数点を(int)でキャストしている関係で、わずかにズレが蓄積します。PICOMでは1曲の長さが数分程度を想定しているので実害はありませんが、10分を超える曲では1サンプル分のズレが聴感上のチリッというノイズになることがあります。正確にやるなら累積誤差を各ステップで補正する方式が必要です。
波形タイプの分岐:Track.Typeごとに異なる音色
チップチューンの音色はSquare Wave、Triangle Wave、ノイズなど、決まったパターンがあります。PICOMではTrackTypes enumからswitchで分岐して波形オブジェクトを生成しています。
var polyphonicTrack = track.Type switch
{
TrackTypes.Square5 => trackBaseSound.CreatePolyphonicTrack(waveStartIndex, new SquareWave(SquareWaveRatio.Point5)),
TrackTypes.Square25 => trackBaseSound.CreatePolyphonicTrack(waveStartIndex, new SquareWave(SquareWaveRatio.Point25)),
TrackTypes.Square125 => trackBaseSound.CreatePolyphonicTrack(waveStartIndex, new SquareWave(SquareWaveRatio.Point125)),
TrackTypes.Triangle => trackBaseSound.CreatePolyphonicTrack(waveStartIndex, new TriangleWave()),
TrackTypes.PseudoTriangle => trackBaseSound.CreatePolyphonicTrack(waveStartIndex, new PseudoTriangleWave()),
TrackTypes.LowbitNoise => trackBaseSound.CreatePolyphonicTrack(waveStartIndex, new LowBitNoiseWave()),
_ => trackBaseSound.CreatePolyphonicTrack(waveStartIndex, new SquareWave(SquareWaveRatio.Point5))
};
矩形波はデューティ比(パルス幅の比率)違いで3種類入れています。50% / 25% / 12.5%でそれぞれ倍音構成が変わるので、音色の違いが聞いてわかるはずです。
バッファリング生成:WASMを固めないために
PICOMの初期実装ではまず曲全体のPCMを一気に生成していたのですが、長い曲だとBlazor WebAssemblyのUIスレッドが秒単位で固まる現象に遭遇しました。
解決策はバッファリング生成です。SoundMakerのGenerateBufferedStereoWaveは、指定サンプル数(PICOMでは44100、つまり1秒分)ごとにPCMチャンクをyieldするIEnumerableを返します。これを1個ずつ処理して連結することで、進捗表示も自然に実装できました。
ちなみに、この方法だけでは完全に画面側のフリーズを解消することはできなかったため、マルチスレッド化を行うことで完全に解消できました。
Blazor WASMマルチスレッド化の詳細はこちら⬇️

var buffers = trackBaseSound.GenerateBufferedStereoWave(startIndex, 44100);
var pcmChunks = new List<byte[]>();
int totalPcmSize = 0;
int chunkIndex = 0;
var totalSamples = trackBaseSound.GetAllTracks()
.Where(t => t.Count != 0)
.Max(t => t.EndIndex) - startIndex;
int estimatedChunks = Math.Max(1, (int)Math.Ceiling(totalSamples / 44100.0));
foreach (var wave in buffers)
{
var chunk = wave.GetBytes(soundFormat.BitRate);
pcmChunks.Add(chunk);
totalPcmSize += chunk.Length;
chunkIndex++;
progress?.Report((double)chunkIndex / estimatedChunks);
}
IProgress<double>で外から進捗を受け取れるようにしています。UI側ではこのコールバックでプログレスバーを動かします。
- 長い曲でもUIが固まらない
- 進捗表示を自然に実装できる
- 途中でキャンセル可能(PICOMではまだ未実装だが拡張しやすい)
- コードが若干複雑になる
- チャンク境界での波形接続を考慮する必要がある
一番の収穫はSoundMakerのAPIを自分でドッグフーディングできたこと
今回の実装で一番の収穫は、自作ライブラリSoundMakerのAPI設計の粗さに気付けたことです。以前の記事にも書きましたが、SoundMakerは最初に楽譜をそのままデータ構造にしてしまっていて、「音を配置する」という自然な操作がやや不自然な手順になっていました。
SoundMakerのドッグフーディング体験の詳細はこちら⬇️

PICOMで実際に使ってみると、たとえば音量設定を各コンポーネント(Note / Tie / Tuplet)に対して個別に行う必要があって、上のSetVolumeToのような再帰メソッドを自分で書く羽目になります。これはSoundMaker側に「PolyphonicTrackの全コンポーネントに一括で音量を設定する」APIがあれば解決する話で、ライブラリ側の改善点として積み残しています。
まとめ
- サウンド生成パイプラインは「フォーマット構築 → トラック生成 → 音符追加 → バッファPCM生成 → WAVラップ」の5段階
- 128分音符位置とサンプル位置の変換は、tempo × samplingFrequencyの式で導出できる
- Blazor WebAssemblyではバッファリング生成でないとUIスレッドが固まる
- 自作OSSを自分で使うと、APIの粗さが必ず見えるはず!




