ogg-opus-decoder
Version:
Web Assembly streaming Ogg Opus decoder
227 lines (187 loc) • 6.47 kB
JavaScript
import { WASMAudioDecoderCommon } from "@wasm-audio-decoders/common";
import { OpusDecoder, OpusDecoderWebWorker } from "opus-decoder";
import CodecParser, {
codecFrames,
header,
channels,
streamCount,
coupledStreamCount,
channelMappingTable,
preSkip,
isLastPage,
absoluteGranulePosition,
data,
totalSamples,
} from "codec-parser";
// prettier-ignore
const simd=async()=>WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,5,1,96,0,1,123,3,2,1,0,10,10,1,8,0,65,0,253,15,253,98,11]))
export default class OggOpusDecoder {
constructor(options = {}) {
this._sampleRate = options.sampleRate || 48000;
this._speechQualityEnhancement = options.speechQualityEnhancement;
this._forceStereo =
options.forceStereo !== undefined ? options.forceStereo : false;
this._onCodec = (codec) => {
if (codec !== "opus")
throw new Error(
"ogg-opus-decoder does not support this codec " + codec,
);
};
// instantiate to create static properties
new WASMAudioDecoderCommon();
this._useMLDecoder = ["lace", "nolace"].includes(
this._speechQualityEnhancement,
);
this._decoderLibraryLoaded = this._loadDecoderLibrary();
this._ready = this._init();
}
_initDecoderClass() {
this._decoderClass = this._useMLDecoder
? this.OpusMLDecoder
: this.OpusDecoder;
}
async _loadDecoderLibrary() {
if (this._useMLDecoder) {
const simdSupported = await simd();
if (simdSupported) {
const { OpusMLDecoder, OpusMLDecoderWebWorker } = await import(
/* webpackChunkName: "opus-ml" */ "@wasm-audio-decoders/opus-ml"
);
this.OpusMLDecoder = OpusMLDecoder;
this.OpusMLDecoderWebWorker = OpusMLDecoderWebWorker;
} else {
console.warn(
`ogg-opus-decoder: This platform does not support WebAssembly SIMD; { speechQualityEnhancements: '${this._speechQualityEnhancement}' } has been disabled`,
);
this._useMLDecoder = false;
}
}
this.OpusDecoder = OpusDecoder;
this.OpusDecoderWebWorker = OpusDecoderWebWorker;
this._initDecoderClass();
}
async _init() {
if (this._decoder) await this._decoder.free();
this._decoder = null;
this._codecParser = new CodecParser("application/ogg", {
onCodec: this._onCodec,
enableFrameCRC32: false,
});
}
async _instantiateDecoder(header) {
this._totalSamplesDecoded = 0;
this._preSkip = header[preSkip];
this._channels = this._forceStereo ? 2 : header[channels];
await this._decoderLibraryLoaded;
this._decoder = new this._decoderClass({
channels: header[channels],
streamCount: header[streamCount],
coupledStreamCount: header[coupledStreamCount],
channelMappingTable: header[channelMappingTable],
preSkip: Math.round((this._preSkip / 48000) * this._sampleRate),
sampleRate: this._sampleRate,
speechQualityEnhancement: this._speechQualityEnhancement,
forceStereo: this._forceStereo,
});
await this._decoder.ready;
}
get ready() {
return this._ready;
}
async reset() {
this._ready = this._init();
await this._ready;
}
free() {
this._ready = this._init();
}
async _decode(oggPages) {
let opusFrames = [],
allErrors = [],
allChannelData = [],
samplesThisDecode = 0,
decoderReady;
const flushFrames = async () => {
if (opusFrames.length) {
await decoderReady;
const { channelData, samplesDecoded, errors } =
await this._decoder.decodeFrames(opusFrames);
allChannelData.push(channelData);
allErrors.push(...errors);
samplesThisDecode += samplesDecoded;
this._totalSamplesDecoded += samplesDecoded;
opusFrames = [];
}
};
for (let i = 0; i < oggPages.length; i++) {
const oggPage = oggPages[i];
// only decode Ogg pages that have codec frames
const frames = oggPage[codecFrames].map((f) => f[data]);
if (frames.length) {
opusFrames.push(...frames);
if (!this._decoder)
// wait until there is an Opus header before instantiating
decoderReady = await this._instantiateDecoder(
oggPage[codecFrames][0][header],
);
}
if (oggPage[isLastPage]) {
// decode anything left in the current ogg file
await flushFrames();
// in cases where BigInt isn't supported, don't do any absoluteGranulePosition logic (i.e. old iOS versions)
if (
oggPage[absoluteGranulePosition] !== undefined &&
allChannelData.length
) {
const totalDecodedSamples_48000 =
(this._totalSamplesDecoded / this._sampleRate) * 48000;
// trim any extra samples that are decoded beyond the absoluteGranulePosition, relative to where we started in the stream
const samplesToTrim = Math.round(
((totalDecodedSamples_48000 - oggPage[totalSamples]) / 48000) *
this._sampleRate,
);
const channelData = allChannelData[allChannelData.length - 1];
if (samplesToTrim > 0) {
for (let i = 0; i < channelData.length; i++) {
channelData[i] = channelData[i].subarray(
0,
channelData[i].length - samplesToTrim,
);
}
}
samplesThisDecode -= samplesToTrim;
this._totalSamplesDecoded -= samplesToTrim;
}
// reached the end of an ogg stream, reset the decoder
await this.reset();
}
}
await flushFrames();
return [
allErrors,
allChannelData,
this._channels,
samplesThisDecode,
this._sampleRate,
16,
];
}
async decode(oggOpusData) {
const decoded = await this._decode([
...this._codecParser.parseChunk(oggOpusData),
]);
return WASMAudioDecoderCommon.getDecodedAudioMultiChannel(...decoded);
}
async decodeFile(oggOpusData) {
const decoded = await this._decode([
...this._codecParser.parseAll(oggOpusData),
]);
await this.reset();
return WASMAudioDecoderCommon.getDecodedAudioMultiChannel(...decoded);
}
async flush() {
const decoded = await this._decode([...this._codecParser.flush()]);
await this.reset();
return WASMAudioDecoderCommon.getDecodedAudioMultiChannel(...decoded);
}
}