貪欲ループではなくビームサーチ
スケールファクターとグローバルゲインを同時に探索し、最初に予算へ収まった候補で 確定せず、バンドごとに複数の経路を残します。幅はeffort値で決まります。
MPEG-1/2/2.5 Layer III エンコーダー · C++20 · WebAssembly
rmpsは「ビットに収まる量子化」ではなく「いちばん良く聴こえる量子化」を探します。 スケールファクターとグローバルゲインをビームサーチし、生の誤差ではなく マスキングモデルで採点して、全グラニュールの全バンドを決定します。
MP3エンコーダーはどれも同じ問いに答えています。各バンドをどこまで粗く量子化したら 聴こえてしまうのか。rmpsの答え方が違うのは、次の4か所です。
スケールファクターとグローバルゲインを同時に探索し、最初に予算へ収まった候補で 確定せず、バンドごとに複数の経路を残します。幅はeffort値で決まります。
探索はNMR(雑音対マスク比)とA特性SDRの混合を最小化します。閾値より下の歪みは コストゼロ、上に出た分だけが代価を払う対象です。
知覚SDRを指定すると、それを保つためにビットレートを法定範囲いっぱいまで動かします。 サイズを決め打ちしたいときのCBRとPID制御のABRも用意しています。
ウィンドウ切替は高域主体のアタック(ハイハット、スネアの立ち上がり、シンバル)で 作動します。低音の厚いヒットはロングブロックのまま、サステインに必要な周波数分解能を保ちます。
各段が次の段に渡ります。教科書的なエンコーダーと最後の2段が違ってくる理由が、 マスキングモデルです。
エンコーダー全体がWebAssemblyにコンパイルされます。読み込みもエンコードも ダウンロードもページ上で完結し、アップロードもサーバーもありません。
import { encodeAudioBuffer } from './rmps.js'; const ac = new AudioContext(); const buf = await ac.decodeAudioData(bytes); const blob = await encodeAudioBuffer(buf, { quality: 70, // 1..100 mode: 'jointStereo', onProgress: ({ ratio }) => bar.value = ratio, });
エンコードはCPUバウンドかつシングルスレッドです。ごく短い素材以外は同梱の モジュールワーカーで実行してください。数分の音声で数秒間CPUを占有するため、 メインスレッドだと目に見えて固まります。
const enc = await RmpsEncoder.create({ channels: 2, sampleRate: 44100, bitrate: 192, }); for (let p = 0; p < L.length; p += enc.samplesPerPass) { sink(enc.encode(L.subarray(p, p + spp), R.subarray(p, p + spp))); } const blob = enc.finish(); // flush + Xingヘッダ enc.destroy();
encode()が0バイトを返すのは正常です。 1フレーム先読みで保持するうえ、
リザーバの出力も遅延します。取りこぼしではなく、後続の呼び出しか
finish() で出てきます。finish() は実際の再生時間と
シークテーブルでXingヘッダを書き直す役割も担うので、必ず呼んでください。