133 lines
4.4 KiB
JavaScript
133 lines
4.4 KiB
JavaScript
// 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.');
|