// 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.');