アキゾラソフトのロゴアキゾラソフト技術 × キャリア × WEBツール

SoundMakerでチップチューン音声パイプラインの作り方

2026-05-31
Contents

はじめに

チップチューン音楽を作成できるWebアプリ「PICOM」では、チップチューンの音を自前で合成しています。その心臓部で使っているのが、私が以前から作っている自作OSSのSoundMakerです。

この記事では、楽譜データ(音符・休符・連符など)からPCM波形を生成し、最終的にWAVバイナリに落とすまでのパイプラインを紹介します。SoundMakerのドッグフーディングの話は以前書きましたが、今回は「実装そのもの」にフォーカスします。

📌この記事の要約

SoundMakerを使って楽譜データからPCM波形を合成する実装を紹介します。FormatBuilderでサウンドフォーマットを構築し、TrackBaseSoundにトラックを載せ、バッファリング方式で1秒ごとにPCMチャンクを取り出してWAVバイナリに結合します。プレビュー用の単音はキャッシュすることで、ユーザーが音符を置くたびに再計算しない工夫も解説します。

パイプラインの全体像

まずは全体の流れです。

  1. FormatBuilderでサウンドフォーマット(サンプリングレート・ビット深度・チャンネル数)を構築
  2. TrackBaseSoundを作り、各トラックの波形タイプ(矩形波・三角波・ノイズなど)を設定
  3. 楽譜モデル(PICOMのNote / Rest / Tie / Tuplet)をSoundMakerの音符オブジェクトに変換
  4. サンプル位置単位でPolyphonicTrackに音符を追加
  5. GenerateBufferedStereoWaveでPCMチャンクをストリーム生成
  6. チャンクを連結して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 WaveTriangle 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個ずつ処理して連結することで、進捗表示も自然に実装できました。

ちなみに、この方法だけでは完全に画面側のフリーズを解消することはできなかったため、マルチスレッド化を行うことで完全に解消できました。

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は最初に楽譜をそのままデータ構造にしてしまっていて、「音を配置する」という自然な操作がやや不自然な手順になっていました。

PICOMで実際に使ってみると、たとえば音量設定を各コンポーネント(Note / Tie / Tuplet)に対して個別に行う必要があって、上のSetVolumeToのような再帰メソッドを自分で書く羽目になります。これはSoundMaker側に「PolyphonicTrackの全コンポーネントに一括で音量を設定する」APIがあれば解決する話で、ライブラリ側の改善点として積み残しています。

まとめ

  • サウンド生成パイプラインは「フォーマット構築 → トラック生成 → 音符追加 → バッファPCM生成 → WAVラップ」の5段階
  • 128分音符位置とサンプル位置の変換は、tempo × samplingFrequencyの式で導出できる
  • Blazor WebAssemblyではバッファリング生成でないとUIスレッドが固まる
  • 自作OSSを自分で使うと、APIの粗さが必ず見えるはず!
プロフィール画像
WRITTEN BY
あきぞら

東京都市大学 情報工学部在籍(2027年卒業予定)。
中学時代よりC#および.NETを用いた個人開発をスタート。現在はWebアプリエンジニアとして活動し、国内大手SaaS企業より内定。 培った技術の活用法から、新卒エンジニアとしてのキャリア形成、生産性を高めるITツールの導入まで、ITに興味のある方や現役エンジニアに役立つバラエティ豊かな情報を発信しています。