WebAssembly ガイド
libsonare は WebAssembly にコンパイルでき、ブラウザで直接オーディオ解析ができます。重要な原則として、その API が扱うのは生の .mp3/.wav ファイルではなく、デコード済みのオーディオサンプル(数値が並んだモノラルの Float32Array)です。サンプルを得る方法は 2 通りで、Web Audio API や別の JavaScript デコーダで自分でファイルをデコードするか、エンコード済みバイト列を Audio.fromMemory* ヘルパーに渡してデコードを任せます。全体の流れは下の表のとおりです。
このページは、ブラウザアプリを作る人向けです。Python スクリプト、ターミナルのバッチ処理、ネイティブデスクトップツールを作る場合は、先に はじめに で別の実行環境を選んでください。
このページでは、セットアップ、単発呼び出しの API、マスタリングを扱います。Web Worker との連携、パフォーマンスチューニング、React の例は WASM の応用的な使い方 を、ライブのストリーミング解析、逆再構成、リアルタイムボイスチェンジャーは WASM のストリーミングとリアルタイム処理 を参照してください。
ブラウザでの考え方
| 手順 | 内容 |
|---|---|
| 1. ファイルを取得する | fetch、<input type="file">、ドラッグ & ドロップ、その他のブラウザ入力を使う |
| 2. 音声をデコードする | Audio.fromMemory(...)、Audio.fromMemoryWithBrowserFallback(...)、AudioContext.decodeAudioData(...)、または独自のデコーダを使う |
| 3. サンプルを選ぶ | 1 つのモノラルチャンネルを渡す、ステレオを自分でダウンミックスする、または対応するステレオ API を使う |
| 4. libsonare を呼ぶ | サンプルと sampleRate を解析、編集、マスタリング、ミキシング API に渡す |
初学者がつまずきやすい点は、MP3 の ArrayBuffer をそのまま解析関数へ渡してしまうことです。先にデコードしてください。ブラウザ版 libsonare が扱うのは、圧縮ファイルのバイト列ではなく PCM サンプルです。
セットアップで問題が起きたときは、まず次の 3 点を確認してください。
- DSP 関数を呼ぶ前に
await init()が完了している。 sampleRateには、デコードしたサンプルのレート(通常はaudioBuffer.sampleRate)を渡している。- サンプル配列は PCM 音声(
Float32Array)であり、エンコード済みファイルのバイト列ではない。
Float32Array・PCM・モノラル・ダウンミックスとは?
- PCM サンプル は、圧縮されていない生の波形で、振幅値の長い列です。MP3/WAV のファイルは圧縮・梱包されたバイト列で、デコードすると PCM になります。
Float32Arrayは、そのサンプルを 32bit 浮動小数点(通常 −1〜1 の範囲)で 1 サンプル 1 要素として保持する、Web Audio API が使う JavaScript の typed array です。libsonare のブラウザ API はこれをそのまま受け取ります。- モノラル/ダウンミックス: モノラルは 1 チャンネルです。ステレオは左右の独立したチャンネルを持ち、ダウンミックスはそれらを 1 つにまとめます(通常は平均)。これでモノラル API に 1 チャンネルを渡せます。
このページで身につくこと
このページを読むと、次のことを判断・実装できるようになります。
- WASM パッケージを正しくインストールし、初期化できる。
- ブラウザ上のファイルを PCM へデコードし、正しいチャンネルとサンプルレートの組を libsonare に渡せる。
- 1 回呼び出しの関数、
Audio、StreamAnalyzer、StreamingMasteringChain、Mixer、RealtimeEngineを使い分けられる。 - ブラウザアプリとして出す前に、バンドルサイズ、Worker、AudioWorklet のトレードオフを理解できる。
インストール
npm/yarn
npm install @libraz/libsonareyarn add @libraz/libsonarepnpm add @libraz/libsonareCDN
<script type="module">
import { init, detectBpm } from 'https://unpkg.com/@libraz/libsonare';
</script>基本的な使い方
import { init, detectBpm, detectKey, analyze } from '@libraz/libsonare';
async function analyzeAudio() {
// WASM モジュールを初期化
await init();
// AudioContext からオーディオデータを取得
const audioCtx = new AudioContext();
const response = await fetch('music.mp3');
const arrayBuffer = await response.arrayBuffer();
const audioBuffer = await audioCtx.decodeAudioData(arrayBuffer);
// 1 つのモノラルチャンネルを取得。ステレオ両方を反映したい場合は明示的にダウンミックスします。
const samples = audioBuffer.getChannelData(0);
const sampleRate = audioBuffer.sampleRate;
// BPM を検出
const bpm = detectBpm(samples, sampleRate);
console.log(`BPM: ${bpm}`);
// キーを検出
const key = detectKey(samples, sampleRate);
console.log(`キー: ${key.name}`);
// まとめて解析
const result = analyze(samples, sampleRate);
console.log(result);
}CLI での同等操作
同じ 1 ファイル確認を sonare コマンドラインで行う場合です(ブラウザでは使えません)。
sonare bpm music.mp3
sonare key music.mp3
sonare analyze music.mp3 --json下のデモは、同じデータの流れを視覚化したものです。デコード済みサンプルを入力し、時間 × 周波数の表示を出力します。ブラウザページでこの種の結果を描画できれば、WASM パッケージの読み込み、初期化、デコード、サンプルレートの受け渡しがつながっていると確認できます。
ブラウザビルドには librosa 互換ヘルパーも含まれるため、既存の librosa のレシピをそのまま移植できます。用途別の関数一覧はヘルパーと変換に、librosa との対応関係は librosa 互換性にまとめています。
ブラウザ内ミキシング
WASM パッケージからミキシングエンジンも使えます。ステムを一括でレンダーするだけなら mixStereo(...)、バス、センド、インサートオートメーション、ゴニオメーター、ストリップメーターが必要ならシーン JSON から作る Mixer を使います。
import { init, Mixer, mixStereo, mixingScenePresetJson } from '@libraz/libsonare';
await init();
const rendered = mixStereo([vocalL, musicL], [vocalR, musicR], sampleRate, {
faderDb: [-3, -12],
pan: [0, -0.2],
width: [1, 0.9],
});
const mixer = Mixer.fromSceneJson(mixingScenePresetJson('vocalReverbSend'), sampleRate, 512);
mixer.scheduleFaderAutomation(0, sampleRate * 4, -6, 's-curve');
const block = mixer.processStereo([vocalBlockL, musicBlockL], [vocalBlockR, musicBlockR]);
const meter = mixer.stripMeter(0, 'postFader');
mixer.delete();詳しくは ミキシングエンジン を参照してください。
CLI での同等操作
組み込みミキサーシーンを sonare コマンドラインでレンダリングする場合です。
sonare mix \
--preset vocalReverbSend \
--input vocal.wav \
--input music.wav \
-o mixed.wavAudio クラス
スタンドアロン関数の代わりに、Audio クラスをオブジェクト指向的に使うこともできます。サンプルとサンプルレートを内部で保持するため、毎回の呼び出しで渡し直す必要がありません。
import { init, Audio } from '@libraz/libsonare';
await init();
const audioCtx = new AudioContext();
const response = await fetch('music.mp3');
const arrayBuffer = await response.arrayBuffer();
const audioBuffer = await audioCtx.decodeAudioData(arrayBuffer);
// Audio インスタンスを作成
const audio = Audio.fromBuffer(
audioBuffer.getChannelData(0),
audioBuffer.sampleRate
);
// 解析
const bpm = audio.detectBpm();
const key = audio.detectKey();
const result = audio.analyze();
// エフェクト
const { harmonic, percussive } = audio.hpss();
const stretched = audio.timeStretch(1.5);
const shifted = audio.pitchShift(2);
// 特徴量
const mel = audio.melSpectrogram();
const mfcc = audio.mfcc();
const chroma = audio.chroma();
const pitch = audio.pitchPyin();
console.log(`BPM: ${bpm}, キー: ${key.name}`);
console.log(`中央値ピッチ: ${pitch.medianF0.toFixed(1)} Hz`);CLI での同等操作
同じ呼び出しを Python CLI で行う場合です。
sonare analyze music.mp3 --json
sonare hpss music.mp3 -o separated --json
sonare pitch-shift music.wav --semitones 2 -o shifted.wav
sonare pitch music.mp3 --algorithm pyin --jsonインスタンスメソッドの一覧は Audio クラスのリファレンスを参照してください。
ブラウザ内マスタリング
/ja/mastering デモは、このページで説明しているものと同じ WASM パッケージを使用しています。音源のデコードはブラウザで行い、マスタリング処理は Web Worker で実行し、レンダリング後の WAV と JSON レポートはローカルで生成されます。masterAudio、masteringChain、名前付きプロセッサはマスタリングのリファレンスで説明しています。
実装の詳細は マスタリング実装, ブラウザ内ローカル処理, マスタリングチェーン, ステレオ、リミッター、ラウドネスコントロール を参照してください。
マスタリング API には、JSON ベースのアシスタント出力、音源プロファイル、配信プラットフォーム別のプレビューを返す masteringAssistantSuggest(...)、masteringAudioProfile(...)、masteringStreamingPreview(...) も含まれます。
CLI での同等操作
シンプルなラウドネス正規化マスターを sonare コマンドラインで作る場合です。
sonare mastering track.wav --target-lufs -14 --ceiling-db -1 -o master.wavファイル入力
WASM の多くの API はデコード済み PCM サンプルを受け取ります。エンコード済みバイト列を使う場合、WAV/MP3 なら Audio.fromMemory(...) を使います。AAC、OGG、FLAC など同梱デコーダが読めない形式も扱いたい場合は、Audio.fromMemoryWithBrowserFallback(...) を使うと、必要に応じて AudioContext.decodeAudioData() によるブラウザ側デコードへ切り替わります。
async function analyzeFile(file: File) {
await init();
const audioCtx = new AudioContext();
const arrayBuffer = await file.arrayBuffer();
const audioBuffer = await audioCtx.decodeAudioData(arrayBuffer);
const samples = audioBuffer.getChannelData(0);
return analyze(samples, audioBuffer.sampleRate);
}
// ファイル入力での使用
const input = document.querySelector('input[type="file"]');
input.addEventListener('change', async (e) => {
const file = e.target.files[0];
const result = await analyzeFile(file);
console.log(`BPM: ${result.bpm}`);
});全チャンネルを保持する
Audio.fromMemory(...) はマルチチャンネルのファイルをデコードしながらモノラルに畳み込みます。チャンネルを保持するには decodeChannels でデコードします。
| 関数 | チャンネルの保持 | デコーダー | 戻り値 |
|---|---|---|---|
Audio.fromMemory | しない(モノラルに畳み込み) | WASM 内の WAV/MP3 | Audio |
Audio.fromMemoryWithBrowserFallback | しない(モノラルに畳み込み) | WASM、次に decodeAudioData | Promise<Audio> |
decodeChannels | する | WASM 内の WAV/MP3 | { sampleRate, channels } |
decodeChannelsWithBrowserFallback(bytes, options?) | する | WASM、次に decodeAudioData(AAC、OGG、FLAC) | Promise<{ sampleRate, channels }> |
downmix(channels, targetLayout, options?) | 狭める | なし | Float32Array[] |
channels は、元のチャンネル順(5.1 は L R C LFE Ls Rs)の Float32Array の配列です。downmix はこれを、targetLayout で指定したより狭いレイアウトへ折りたたみます。
targetLayout | 結果 |
|---|---|
0 | モノラル |
1 | ステレオ |
2 | 5.1 |
3 | 7.1 |
規則は ITU-R BS.775(センターとサラウンドを -3 dB、LFE は除外)で、アップミックスはできません。元のレイアウトはチャンネル数(1、2、6、8)から判断し、それ以外のチャンネル数はモノラルへの折りたたみ(単純平均)だけが可能です。折りたたみは正規化されないため、フルスケールに近いプレーンが複数あるソースはフルスケールを超えることがあります(1.0 の 6 プレーンはステレオ目標で 2.414 になります)。省略可能な第 3 引数 { normalize, includeLfe } で挙動を変えられます。normalize: true は最悪ケース(全入力がフルスケールで同相)でも 1 以下に収まるよう折りたたみをスケールします(ラウドネスを合わせるのではなくピークで割る方式です)。includeLfe: true は LFE プレーンを捨てずに -3 dB でフロントペアへ加えます。どちらも既定は false です。...WithBrowserFallback 系の関数はマルチチャンネルの AudioBuffer に同じ規則を適用するので、どちらのデコーダーが動いても同じサンプルになります。downmix は、32 ビット整数の範囲の整数でない targetLayout に RangeError を、数値でないものに TypeError を投げます。
import { decodeChannels, downmix, init } from '@libraz/libsonare';
await init();
const { sampleRate, channels } = decodeChannels(new Uint8Array(await file.arrayBuffer()));
const stereo = channels.length > 2 ? downmix(channels, 1) : channels;クリップを OPFS に保存する
writeOpfsClip(path, channels) は、チャンネルごとの Float32Array を、ヘッダーなしのリトルエンディアン・インターリーブ float32 として Origin Private File System に書き込みます。OPFS のクリップページリーダーが読む形式と同じです。importOpfsClip(path, source) は ArrayBuffer、Blob、AudioBuffer を受け取り、全チャンネルを保ち、リサンプリングせずに元の sampleRate を返します。どちらも OPFS ワーカーで動き、開いているページプロバイダーが読んでいるパスへ書くと InvalidState を投げます。そのクリップをエンジンへページングする方法はリアルタイムとストリーミングを参照してください。
進捗レポート
import { init, analyzeWithProgress } from '@libraz/libsonare';
await init();
const result = analyzeWithProgress(samples, sampleRate, (progress, stage) => {
const percent = Math.round(progress * 100);
console.log(`${stage}: ${percent}%`);
// UI を更新
progressBar.style.width = `${percent}%`;
statusText.textContent = stage;
});キャンセル
時間のかかるオフラインの解析・マスタリング呼び出しは cancel コールバックを受け取ります。onProgress が進捗を報告するのと同じ境界でポーリングされるため、間違ったファイルを読み込んでしまったユーザーがレンダリングの完了を待つ必要はありません。true を返すと呼び出しは中断され、コード 8(Cancelled)の SonareError で例外になります。キャンセルされた呼び出しは出力を確保しません。途中結果を読むことはできないので、「マスターが途中まで出来た」ではなく「何も起きなかった」として扱ってください。完全な例は長い処理をキャンセルするを参照してください。
ステレオからモノラルへの変換
async function getMonoSamples(audioBuffer: AudioBuffer): Promise<Float32Array> {
if (audioBuffer.numberOfChannels === 1) {
return audioBuffer.getChannelData(0);
}
// ステレオをモノラルにミックス。5.1 / 7.1 のソースには decodeChannels() と downmix() を使う。
const left = audioBuffer.getChannelData(0);
const right = audioBuffer.getChannelData(1);
const mono = new Float32Array(left.length);
for (let i = 0; i < left.length; i++) {
mono[i] = (left[i] + right[i]) / 2;
}
return mono;
}ブラウザ互換性
同梱モジュールは WebAssembly SIMD でビルドされているため、最小バージョンは SIMD に対応したブラウザで決まります。
| ブラウザ | 最小バージョン |
|---|---|
| Chrome | 91+ |
| Edge | 91+ |
| Firefox | 89+ |
| Safari | 16.4+ |
要件: WebAssembly SIMD、BigInt、Web Audio API。JavaScript バンドルのターゲットは ECMAScript 2020 です。
パッケージの構成物
公開パッケージには、連携するいくつかの構成物が含まれます。
- メインモジュール:
sonare.jsとsonare.wasm。解析・マスタリング・ミキシング・編集の各 API を支える Emscripten ビルドです。 - メイン API エントリ: パッケージの
index(index.js/index.d.ts)はimport ... from '@libraz/libsonare'を支える tsup バンドルです。解析・マスタリング・ミキシング・編集の全 API を公開します。 - AudioWorklet エントリ:
worklet.js/worklet.d.ts。独立した自己完結型の tsup バンドル(コード分割なし、AudioWorkletGlobalScopeへの移植を想定)で、SonareEngineAPI、worklet プロセッサクラスとその登録ヘルパー、SonareStreamAnalyzerNode(オーディオグラフの枝のライブ解析)、リングバッファプロトコル(engineMixerMasterTarget、ENGINE_MIXER_PARAM_FADER_DB、ENGINE_MIXER_PARAM_PAN、SONARE_METER_RING_PROTOCOL_VERSIONを含む)を収録します。worklet レルムが独自の WASM インスタンスを初期化できるよう、メインエントリからinit、isInitialized、EXPECTED_ABI_VERSIONのみを再エクスポートします。再生用とミキサー用のプロセッサは、プロセッサオプションのwasmBinaryでコンパイル済みモジュールを受け取り、worklet レルムにモジュールがなければ自分で初期化して、準備ができた時点でreadyを送ります。ボイスチェンジャー、ミキサー、エンジン、再生のプロセッサはAudioWorkletGlobalScopeのサンプルレートで動作します。異なるsampleRateオプションはRangeErrorで拒否され、worklet スコープの外ではこのオプションが必須です。共有メモリのリングはそれぞれcreateSonare*RingBufferヘルパーで作り、worklet 側とメインスレッド側のコードは同じパッケージバージョンから読み込んでください(メーターリングはプロトコル 3、スペクトラム、スコープ、テレメトリのリングはプロトコル 1)。手でサイズを決めたバッファは、プロセッサの構築時に拒否されます。オーディオスレッドで起きた失敗は、クラスとSonareErrorのコードを保ったままメインスレッドに届きます。 - 解析専用モジュール:
@libraz/libsonare/analysisエントリの背後にあるsonare-analysis.js/sonare-analysis.wasm(生のアセットは@libraz/libsonare/wasm/analysis)。マスタリング・ミキシング・リアルタイム・プロジェクトのバインディングを外してコンパイルしています。CI はサイズをレポートに記録しますが、増加だけでビルドを失敗させません。 - オフライン Worker エントリ:
@libraz/libsonare/workerの背後にあるworker.js。OfflineWorkerClientの Worker 側です。 - ボーカル編集 Worker エントリ:
@libraz/libsonare/vocal-edit-workerの背後にあるvocal_edit_worker.js。VocalEditWorkerClientの Worker 側です。 - ボイスチェンジャーの JSON Schema: 2 つのプリセットスキーマが
schemas/以下に同梱されるため、ホストは何も取得せずにプリセット文書を検証できます。
モジュールを自分でインスタンス化する場合
sonare.js / sonare.wasm は、パッケージがラップしている Emscripten モジュールそのものです。通常のアプリケーションがこれに直接触れる必要はありません。@libraz/libsonare から import すればモジュールは初期化され、レルムごとに 1 インスタンスが保たれます。こちらがサポート対象の入口です。独自ローダー、非標準のバンドル先、ヒープを自前で管理するホストなど、モジュールを自分でインスタンス化する場合にかぎり、知っておくべき入力形式がひとつあります。
マスタリングチェーンのバインディングはフラット化済みの形式だけを受け取ります
モジュールのマスタリングチェーンのエントリが受け付ける設定形式はひとつだけです。コア自身のパラメータパーサが読む、フラット化されたパラメータ列です。代わりにネストした設定オブジェクトを渡すと、呼び出しは名指しで拒否されます。認識できた部分だけを適用して残りを落とすのではなく、読めない設定を明示したエラーが返ります。
npm パッケージの利用には影響しません。 masteringChain と masterAudio は、ネストした MasteringChainConfig を(リペア段や denoise の設定も含めて)モジュールへ渡す前にフラット化します。したがって @libraz/libsonare から import しているコードは、JavaScript API の記述どおりネストした設定を渡し続けられます。該当するのは、モジュールを直接インスタンス化してネストしたオブジェクトをバインディングにそのまま渡す呼び出し側だけで、その場合も得られるのは、一部だけ適用されたチェーンではなく明確なエラーです。
バンドルサイズ
このサイズ表が対象とするのはメインモジュールとメイン API エントリです。解析専用モジュール、realtime ランタイム、worklet バンドルは別の構成物で、ここには記載していません。解析専用モジュールは、マスタリング・ミキシング・リアルタイム・プロジェクトの各機能を含まない分、sonare.wasm よりかなり小さくなります。
| ファイル | サイズ | Gzip |
|---|---|---|
sonare.js | ~96 KB | ~17 KB |
index.js | ~595 KB | ~118 KB |
sonare.wasm | ~7,808 KB | ~2,656 KB |
| 合計 | ~8,500 KB | ~2,792 KB |
トラブルシューティング
AudioContext が許可されない
モダンブラウザは AudioContext を作成する前にユーザー操作が必要:
document.addEventListener('click', async () => {
const audioCtx = new AudioContext();
await audioCtx.resume();
});クロスオリジンの問題
他のドメインからオーディオを読み込む場合:
const response = await fetch(url, {
mode: 'cors',
credentials: 'omit'
});メモリの問題
非常に長いオーディオファイルの場合、チャンクで解析を検討:
const CHUNK_DURATION = 60; // 秒
for (let start = 0; start < totalDuration; start += CHUNK_DURATION) {
const chunk = samples.slice(
start * sampleRate,
(start + CHUNK_DURATION) * sampleRate
);
// チャンクを解析
}どのエラーが投げられるか
ライブラリが起こす失敗は、数値の code と codeName を持つ構造化された SonareError です。拒否された引数(範囲外の値、未知の ID や名前、型の誤り)は素の RangeError か TypeError になるので、SonareError のハンドラーでは捕捉できません。先行する設定や解放済みのオブジェクトが原因の失敗は、コード付きの InvalidState です。クラスの表、失敗ごとの対応表、3 つのクラスをすべて扱う例はエラーハンドリングにあります。ほかのバインディングでの同じ失敗はエラー契約にまとめています。JavaScript とは別のツリーからビルドされた .wasm は、init() が AbiMismatch(コード 10)を投げます。よくある原因はインストールを参照してください。