KUON DSD の仕組み
世界初のブラウザDSDプレーヤー。Rustで実装したWasm音声エンジン。
DSD とは
DSD(ダイレクト・ストリーム・デジタル)は、Super Audio CD(SACD)に採用された1ビット・超高速シグマデルタ変調フォーマットです。PCMのように離散的なサンプルを保存するのではなく、DSDは非常に高いサンプリング周波数での連続ビットストリームを保存します。
DSD サンプリング周波数:
- DSD64 — 2.8224 MHz (CD周波数の約64倍)
- DSD128 — 5.6448 MHz (CD周波数の約128倍)
- DSD256 — 11.2896 MHz (CD周波数の約256倍)
チャレンジ: ブラウザでDSDを再生することが「不可能」な理由
ブラウザのオーディオAPIはPCM形式向けに設計されており、DSDビットストリーム向けではありません。ブラウザでDSDを再生する際には、いくつかの障壁が現れます:
ネイティブDSDサポートなし — Web Audio APIはPCM(Float32Array)のみを受け入れる
ファイルサイズ: DSDアルバムはギガバイト単位(DSD256以上はさらに大きい)
JavaScriptはMHz周波数のDSP処理に対して、特殊な加速なしでは遅すぎる
フィルター設計: デシメーションフィルターは70dB以上のエイリアシング成分を抑制する必要がある
DSF & DSDIFF: バイナリファイルフォーマット
DSDオーディオは2つのコンテナフォーマットで提供されます。私たちのWasmエンジンは両方をサポートする必要があります:
DSF(DSD Stream File)
シンプルで直線的なフォーマット: DSD Chunk → fmt Chunk → data Chunk. オフセットは固定。
DSDIFF(フィリップス "DSD Interchange File Format")
ネストされたチャンク構造: FRM8(IFF形式64ビットコンテナ) → DSD → PROP → CHNL → CMPR → DSDデータ。
fmt Chunkの重要なバイトオフセット:
- +24: オーディオチャンネル数
- +28: DSDサンプルレートコード
- +32: サンプルあたりのビット数(DSDは常に8)
Rust WebAssemblyエンジン: 核となるイノベーション
DSDをブラウザで動作させるための秘訣は、コンパイルされたRustコードです。理由は以下の通りです:
ゼロコストな抽象化 — オーディオ処理中にガベージコレクションが一時停止しない
SIMD的な最適化 — コンパイラはバイトレベルの演算をベクトル化できる
FIR畳み込みでJavaScriptより10倍高速(ベンチマーク結果)
コンパイルパイプライン
wasm-pack build --target web出力: .wasmバイナリ(~50KB)、JavaScriptグルーコード、TypeScript定義
Signal Flow Architecture
バイトレベル ルックアップテーブル(LUT): 超高速ビット抽出
DSDはサンプルあたり8ビットです。つまり、各バイトは8個のシグマデルタビットをエンコードします。単純なアプローチでは、すべてのバイトの8ビットすべてをループし、それらのフロート値を計算します。これは遅い。
ソリューション: 256エントリのテーブルを事前計算します。各インデックス(0–255)は直接8個のフロート値にマップされます:
LUT[0x55] = [+1.0, +1.0, -1.0, +1.0, +1.0, -1.0, +1.0, +1.0] // バイナリ: 01010101ブランチングなし、ビットシフトオーバーヘッドなし — ただ配列ルックアップ+メモリアクセス。
Blackman-Sinc FIRデシメーションフィルター: 数学的な中心
DSDをPCMに変換するには、高品質なローパスフィルターが必要です。Blackman窓付きsinc FIRフィルターを使用します:
Sinc関数(理想的なローパス)
sinc(x) = sin(πx) / (πx)sinc関数は数学的に「完璧」です — その周波数応答は、ゼロのサイドローブを備えたブリックウォール型のローパスです。
Blackman窓(サイドローブ抑制)
w(n) = 0.42 − 0.5 cos(2πn/N) + 0.08 cos(4πn/N)sincタップに適用すると、Blackman窓は滑らかにロールオフし、-74dBのサイドローブ抑制を実現します。これはエイリアシングアーティファクトを防ぎます。
畳み込み: デシメーション操作
y[n] = Σ h[k] × x[n − k] (各出力サンプルに対して)各出力PCMサンプルに対して、フィルター係数を周辺のDSDビットで乗算し、合計します。タップ数(係数)はデシメーション比に応じてスケーリングします。
DSD64→44.1kHzは~8192タップが必要です。DSD256→44.1kHzは~32,768タップが必要です。
ストリーミングチャンク処理: ギガバイトファイルの処理
高解像度DSD256アルバムは5~8GBになる場合があります。ファイル全体をメモリにロードすることはできません。代わりに、チャンク処理を使用します:
- ユーザーがDSDファイルを選択
- 8MBのチャンクがFileReader.readAsArrayBuffer(blob.slice(offset, offset + 8MB))経由で読み取られます
- 各チャンクはWasm(DSDパーサー→LUT→FIR→Float32出力)によって独立して解析されます
- 結果は成長するFloat32Arrayに累積されます(メモリはわずか~16MBを超えません)
WAVエクスポート: 24ビットPCMエンコーディング
デシメーション後、Float32サンプルは24ビット整数PCMに変換し、RIFF/WAVEコンテナでラップされます:
- Float32クランピング: max(−1.0, min(+1.0, sample))
- Int24に量子化: round(sample × 8388607) — 24ビット符号付き範囲
- サンプルあたり3バイトをパック(Blob.arrayBuffer用リトルエンディアン)
- RIFF/WAVEヘッダー(44バイト)に正しいチャンクサイズを付加します
パフォーマンス: リアルワールドベンチマーク
私たちの実装は巨大なDSDファイルで実戦テストされています:
- DSD64アルバム(2.1GB)→192kHz WAV: ~45秒
- Rust Wasm対JavaScript: FIR畳み込みで~10倍高速
Wasmが勝つ理由:
- FIR処理中にガベージコレクション一時停止がない
- 線形メモリレイアウト — キャッシュフレンドリーな配列アクセス
- 緊いループでのコンパイラSIMD最適化