Source code

Revision control

Copy as Markdown

Other Tools

Test Info: Warnings

// META: global=window
// META: script=/webcodecs/utils.js
// Encode `channels` channels of audio through AudioEncoder and collect all
// output chunks + the first decoderConfig emitted.
async function encode_opus(t, channels) {
const config = {
codec: 'opus',
sampleRate: 48000,
numberOfChannels: channels,
};
const support = await AudioEncoder.isConfigSupported(config);
assert_true(support.supported,
`${channels}ch Opus encoding must be supported`);
let decoder_config = null;
let chunks = [];
const encoder = new AudioEncoder({
output: (chunk, metadata) => {
if (metadata?.decoderConfig) {
if (!decoder_config) decoder_config = metadata.decoderConfig;
}
chunks.push(chunk);
},
error: e => assert_unreached('encoder error: ' + e),
});
encoder.configure(config);
// One second of audio is enough to produce multiple Opus frames.
const data = make_audio_data(0, channels, 48000, 48000);
encoder.encode(data);
await encoder.flush();
encoder.close();
data.close();
return { decoder_config, chunks };
}
// Verify the OpusHead magic bytes ('O','p','u','s','H','e','a','d').
function assert_valid_opushead(description, label) {
const bytes = new Uint8Array(description);
assert_greater_than_equal(bytes.length, 19,
`${label}: description must be at least 19 bytes`);
assert_equals(bytes[0], 0x4f, `${label}: description[0] == 'O'`);
assert_equals(bytes[1], 0x70, `${label}: description[1] == 'p'`);
assert_equals(bytes[2], 0x75, `${label}: description[2] == 'u'`);
assert_equals(bytes[3], 0x73, `${label}: description[3] == 's'`);
assert_equals(bytes[4], 0x48, `${label}: description[4] == 'H'`);
assert_equals(bytes[5], 0x65, `${label}: description[5] == 'e'`);
assert_equals(bytes[6], 0x61, `${label}: description[6] == 'a'`);
assert_equals(bytes[7], 0x64, `${label}: description[7] == 'd'`);
}
promise_test(async t => {
const channels = 16;
const { decoder_config, chunks } = await encode_opus(t, channels);
assert_greater_than(chunks.length, 0, 'encoder must produce output');
assert_not_equals(decoder_config, null,
'encoder must emit decoderConfig in metadata');
assert_equals(decoder_config.codec, 'opus');
assert_equals(decoder_config.numberOfChannels, channels);
assert_equals(decoder_config.sampleRate, 48000);
assert_not_equals(decoder_config.description, null,
'decoderConfig.description must be present for >2ch Opus');
assert_valid_opushead(decoder_config.description, '16ch');
// channel count must be in the description (byte 9, 0-indexed)
const bytes = new Uint8Array(decoder_config.description);
assert_equals(bytes[9], channels,
'OpusHead channel count byte must match numberOfChannels');
}, 'Encoding 16ch Opus produces a valid OpusHead decoderConfig description.');
promise_test(async t => {
const channels = 16;
const { decoder_config, chunks } = await encode_opus(t, channels);
assert_not_equals(decoder_config, null, 'need decoderConfig to test decode');
assert_greater_than(chunks.length, 0, 'need chunks to decode');
let decoded_frames = [];
const decoder = new AudioDecoder({
output: frame => { decoded_frames.push(frame); },
error: e => assert_unreached('decoder error: ' + e),
});
decoder.configure({
codec: 'opus',
sampleRate: 48000,
numberOfChannels: channels,
description: decoder_config.description,
});
for (const chunk of chunks) {
decoder.decode(chunk);
}
await decoder.flush();
decoder.close();
assert_greater_than(decoded_frames.length, 0, 'decoder must produce output');
for (const frame of decoded_frames) {
assert_equals(frame.numberOfChannels, channels,
'decoded frame must have ' + channels + ' channels');
frame.close();
}
}, 'Encoding then decoding 16ch Opus produces frames with 16 channels.');
promise_test(async t => {
const config = {
codec: 'opus',
sampleRate: 48000,
numberOfChannels: 16,
// no description
};
// configure() queues work; the codec-specific failure surfaces asynchronously
// through the error callback.
const error = await new Promise(resolve => {
const decoder = new AudioDecoder({
output: () => {},
error: e => resolve(e),
});
decoder.configure(config);
});
assert_equals(error.name, 'NotSupportedError',
'>2ch Opus without description must fire NotSupportedError');
}, 'Configuring AudioDecoder with 16ch Opus without description must fail.');