diff options
Diffstat (limited to 'testing/web-platform/tests/webcodecs/audio-encoder.https.any.js')
-rw-r--r-- | testing/web-platform/tests/webcodecs/audio-encoder.https.any.js | 572 |
1 files changed, 572 insertions, 0 deletions
diff --git a/testing/web-platform/tests/webcodecs/audio-encoder.https.any.js b/testing/web-platform/tests/webcodecs/audio-encoder.https.any.js new file mode 100644 index 0000000000..7db9148ed5 --- /dev/null +++ b/testing/web-platform/tests/webcodecs/audio-encoder.https.any.js @@ -0,0 +1,572 @@ +// META: global=window +// META: script=/webcodecs/utils.js + +// Merge all audio buffers into a new big one with all the data. +function join_audio_data(audio_data_array) { + assert_greater_than_equal(audio_data_array.length, 0); + let total_frames = 0; + let base_buffer = audio_data_array[0]; + for (const data of audio_data_array) { + assert_not_equals(data, null); + assert_equals(data.sampleRate, base_buffer.sampleRate); + assert_equals(data.numberOfChannels, base_buffer.numberOfChannels); + assert_equals(data.format, base_buffer.format); + total_frames += data.numberOfFrames; + } + + assert_true(base_buffer.format == 'f32' || base_buffer.format == 'f32-planar'); + + if (base_buffer.format == 'f32') + return join_interleaved_data(audio_data_array, total_frames); + + // The format is 'FLTP'. + return join_planar_data(audio_data_array, total_frames); +} + +function join_interleaved_data(audio_data_array, total_frames) { + let base_data = audio_data_array[0]; + let channels = base_data.numberOfChannels; + let total_samples = total_frames * channels; + + let result = new Float32Array(total_samples); + + let copy_dest = new Float32Array(base_data.numberOfFrames * channels); + + // Copy all the interleaved data. + let position = 0; + for (const data of audio_data_array) { + let samples = data.numberOfFrames * channels; + if (copy_dest.length < samples) + copy_dest = new Float32Array(samples); + + data.copyTo(copy_dest, {planeIndex: 0}); + result.set(copy_dest, position); + position += samples; + } + + assert_equals(position, total_samples); + + return result; +} + +function join_planar_data(audio_data_array, total_frames) { + let base_frames = audio_data_array[0].numberOfFrames; + let channels = audio_data_array[0].numberOfChannels; + let result = new Float32Array(total_frames*channels); + let copyDest = new Float32Array(base_frames); + + // Merge all samples and lay them out according to the FLTP memory layout. + let position = 0; + for (let ch = 0; ch < channels; ch++) { + for (const data of audio_data_array) { + data.copyTo(copyDest, { planeIndex: ch}); + result.set(copyDest, position); + position += data.numberOfFrames; + } + } + assert_equals(position, total_frames * channels); + + return result; +} + +promise_test(async t => { + let sample_rate = 48000; + let total_duration_s = 1; + let data_count = 10; + let outputs = []; + let init = { + error: e => { + assert_unreached("error: " + e); + }, + output: chunk => { + outputs.push(chunk); + } + }; + + let encoder = new AudioEncoder(init); + + assert_equals(encoder.state, "unconfigured"); + let config = { + codec: 'opus', + sampleRate: sample_rate, + numberOfChannels: 2, + bitrate: 256000 //256kbit + }; + + encoder.configure(config); + + let timestamp_us = 0; + let data_duration_s = total_duration_s / data_count; + let data_length = data_duration_s * config.sampleRate; + for (let i = 0; i < data_count; i++) { + let data = make_audio_data(timestamp_us, config.numberOfChannels, + config.sampleRate, data_length); + encoder.encode(data); + data.close(); + timestamp_us += data_duration_s * 1_000_000; + } + await encoder.flush(); + encoder.close(); + assert_greater_than_equal(outputs.length, data_count); + assert_equals(outputs[0].timestamp, 0, "first chunk timestamp"); + let total_encoded_duration = 0 + for (chunk of outputs) { + assert_greater_than(chunk.byteLength, 0); + assert_greater_than_equal(timestamp_us, chunk.timestamp); + assert_greater_than(chunk.duration, 0); + total_encoded_duration += chunk.duration; + } + + // The total duration might be padded with silence. + assert_greater_than_equal( + total_encoded_duration, total_duration_s * 1_000_000); +}, 'Simple audio encoding'); + +promise_test(async t => { + let sample_rate = 48000; + let total_duration_s = 1; + let data_count = 10; + let outputs = []; + let init = { + error: e => { + assert_unreached('error: ' + e); + }, + output: chunk => { + outputs.push(chunk); + } + }; + + let encoder = new AudioEncoder(init); + + assert_equals(encoder.state, 'unconfigured'); + let config = { + codec: 'opus', + sampleRate: sample_rate, + numberOfChannels: 2, + bitrate: 256000 // 256kbit + }; + + encoder.configure(config); + + let timestamp_us = -10000; + let data = make_audio_data( + timestamp_us, config.numberOfChannels, config.sampleRate, 10000); + encoder.encode(data); + data.close(); + await encoder.flush(); + encoder.close(); + assert_greater_than_equal(outputs.length, 1); + assert_equals(outputs[0].timestamp, -10000, 'first chunk timestamp'); + for (chunk of outputs) { + assert_greater_than(chunk.byteLength, 0); + assert_greater_than_equal(chunk.timestamp, timestamp_us); + } +}, 'Encode audio with negative timestamp'); + +async function checkEncodingError(config, good_data, bad_data) { + let error = null; + let outputs = 0; + let init = { + error: e => { + error = e; + }, + output: chunk => { + outputs++; + } + }; + let encoder = new AudioEncoder(init); + + + let support = await AudioEncoder.isConfigSupported(config); + assert_true(support.supported) + config = support.config; + + encoder.configure(config); + for (let data of good_data) { + encoder.encode(data); + data.close(); + } + await encoder.flush(); + + let txt_config = "sampleRate: " + config.sampleRate + + " numberOfChannels: " + config.numberOfChannels; + assert_equals(error, null, txt_config); + assert_greater_than(outputs, 0); + encoder.encode(bad_data); + await encoder.flush().catch(() => {}); + assert_not_equals(error, null, txt_config); +} + +function channelNumberVariationTests() { + let sample_rate = 48000; + for (let channels = 1; channels <= 2; channels++) { + let config = { + codec: 'opus', + sampleRate: sample_rate, + numberOfChannels: channels, + bitrate: 128000 + }; + + let ts = 0; + let length = sample_rate / 10; + let data1 = make_audio_data(ts, channels, sample_rate, length); + + ts += Math.floor(data1.duration / 1000000); + let data2 = make_audio_data(ts, channels, sample_rate, length); + ts += Math.floor(data2.duration / 1000000); + + let bad_data = make_audio_data(ts, channels + 1, sample_rate, length); + promise_test(async t => + checkEncodingError(config, [data1, data2], bad_data), + "Channel number variation: " + channels); + } +} +channelNumberVariationTests(); + +function sampleRateVariationTests() { + let channels = 1 + for (let sample_rate = 3000; sample_rate < 96000; sample_rate += 10000) { + let config = { + codec: 'opus', + sampleRate: sample_rate, + numberOfChannels: channels, + bitrate: 128000 + }; + + let ts = 0; + let length = sample_rate / 10; + let data1 = make_audio_data(ts, channels, sample_rate, length); + + ts += Math.floor(data1.duration / 1000000); + let data2 = make_audio_data(ts, channels, sample_rate, length); + ts += Math.floor(data2.duration / 1000000); + + let bad_data = make_audio_data(ts, channels, sample_rate + 333, length); + promise_test(async t => + checkEncodingError(config, [data1, data2], bad_data), + "Sample rate variation: " + sample_rate); + } +} +sampleRateVariationTests(); + +promise_test(async t => { + let sample_rate = 48000; + let total_duration_s = 1; + let data_count = 10; + let input_data = []; + let output_data = []; + + let decoder_init = { + error: t.unreached_func("Decode error"), + output: data => { + output_data.push(data); + } + }; + let decoder = new AudioDecoder(decoder_init); + + let encoder_init = { + error: t.unreached_func("Encoder error"), + output: (chunk, metadata) => { + let config = metadata.decoderConfig; + if (config) + decoder.configure(config); + decoder.decode(chunk); + } + }; + let encoder = new AudioEncoder(encoder_init); + + let config = { + codec: 'opus', + sampleRate: sample_rate, + numberOfChannels: 2, + bitrate: 256000, //256kbit + }; + encoder.configure(config); + + let timestamp_us = 0; + const data_duration_s = total_duration_s / data_count; + const data_length = data_duration_s * config.sampleRate; + for (let i = 0; i < data_count; i++) { + let data = make_audio_data(timestamp_us, config.numberOfChannels, + config.sampleRate, data_length); + input_data.push(data); + encoder.encode(data); + timestamp_us += data_duration_s * 1_000_000; + } + await encoder.flush(); + encoder.close(); + await decoder.flush(); + decoder.close(); + + + let total_input = join_audio_data(input_data); + let frames_per_plane = total_input.length / config.numberOfChannels; + + let total_output = join_audio_data(output_data); + + let base_input = input_data[0]; + let base_output = output_data[0]; + + // TODO: Convert formats to simplify conversions, once + // https://github.com/w3c/webcodecs/issues/232 is resolved. + assert_equals(base_input.format, "f32-planar"); + assert_equals(base_output.format, "f32"); + + assert_equals(base_output.numberOfChannels, config.numberOfChannels); + assert_equals(base_output.sampleRate, sample_rate); + + // Output can be slightly longer that the input due to padding + assert_greater_than_equal(total_output.length, total_input.length); + + // Compare waveform before and after encoding + for (let channel = 0; channel < base_input.numberOfChannels; channel++) { + + let plane_start = channel * frames_per_plane; + let input_plane = total_input.slice( + plane_start, plane_start + frames_per_plane); + + for (let i = 0; i < base_input.numberOfFrames; i += 10) { + // Instead of de-interleaving the data, directly look into |total_output| + // for the sample we are interested in. + let ouput_index = i * base_input.numberOfChannels + channel; + + // Checking only every 10th sample to save test time in slow + // configurations like MSAN etc. + assert_approx_equals( + input_plane[i], total_output[ouput_index], 0.5, + 'Difference between input and output is too large.' + + ' index: ' + i + ' channel: ' + channel + + ' input: ' + input_plane[i] + + ' output: ' + total_output[ouput_index]); + } + } + +}, 'Encoding and decoding'); + +promise_test(async t => { + let output_count = 0; + let encoder_config = { + codec: 'opus', + sampleRate: 24000, + numberOfChannels: 1, + bitrate: 96000 + }; + let decoder_config = null; + + let init = { + error: t.unreached_func("Encoder error"), + output: (chunk, metadata) => { + let config = metadata.decoderConfig; + // Only the first invocation of the output callback is supposed to have + // a |config| in it. + output_count++; + if (output_count == 1) { + assert_equals(typeof config, "object"); + decoder_config = config; + } else { + assert_equals(config, undefined); + } + } + }; + + let encoder = new AudioEncoder(init); + encoder.configure(encoder_config); + + let large_data = make_audio_data(0, encoder_config.numberOfChannels, + encoder_config.sampleRate, encoder_config.sampleRate); + encoder.encode(large_data); + await encoder.flush(); + + // Large data produced more than one output, and we've got decoder_config + assert_greater_than(output_count, 1); + assert_not_equals(decoder_config, null); + assert_equals(decoder_config.codec, encoder_config.codec); + assert_equals(decoder_config.sampleRate, encoder_config.sampleRate); + assert_equals(decoder_config.numberOfChannels, encoder_config.numberOfChannels); + + // Check that description start with 'Opus' + let extra_data = new Uint8Array(decoder_config.description); + assert_equals(extra_data[0], 0x4f); + assert_equals(extra_data[1], 0x70); + assert_equals(extra_data[2], 0x75); + assert_equals(extra_data[3], 0x73); + + decoder_config = null; + output_count = 0; + encoder_config.bitrate = 256000; + encoder.configure(encoder_config); + encoder.encode(large_data); + await encoder.flush(); + + // After reconfiguring encoder should produce decoder config again + assert_greater_than(output_count, 1); + assert_not_equals(decoder_config, null); + assert_not_equals(decoder_config.description, null); + encoder.close(); +}, "Emit decoder config and extra data."); + +promise_test(async t => { + let sample_rate = 48000; + let total_duration_s = 1; + let data_count = 100; + let init = getDefaultCodecInit(t); + init.output = (chunk, metadata) => {} + + let encoder = new AudioEncoder(init); + + // No encodes yet. + assert_equals(encoder.encodeQueueSize, 0); + + let config = { + codec: 'opus', + sampleRate: sample_rate, + numberOfChannels: 2, + bitrate: 256000 //256kbit + }; + encoder.configure(config); + + // Still no encodes. + assert_equals(encoder.encodeQueueSize, 0); + + let datas = []; + let timestamp_us = 0; + let data_duration_s = total_duration_s / data_count; + let data_length = data_duration_s * config.sampleRate; + for (let i = 0; i < data_count; i++) { + let data = make_audio_data(timestamp_us, config.numberOfChannels, + config.sampleRate, data_length); + datas.push(data); + timestamp_us += data_duration_s * 1_000_000; + } + + let lastDequeueSize = Infinity; + encoder.ondequeue = () => { + assert_greater_than(lastDequeueSize, 0, "Dequeue event after queue empty"); + assert_greater_than(lastDequeueSize, encoder.encodeQueueSize, + "Dequeue event without decreased queue size"); + lastDequeueSize = encoder.encodeQueueSize; + }; + + for (let data of datas) + encoder.encode(data); + + assert_greater_than_equal(encoder.encodeQueueSize, 0); + assert_less_than_equal(encoder.encodeQueueSize, data_count); + + await encoder.flush(); + // We can guarantee that all encodes are processed after a flush. + assert_equals(encoder.encodeQueueSize, 0); + // Last dequeue event should fire when the queue is empty. + assert_equals(lastDequeueSize, 0); + + // Reset this to Infinity to track the decline of queue size for this next + // batch of encodes. + lastDequeueSize = Infinity; + + for (let data of datas) { + encoder.encode(data); + data.close(); + } + + assert_greater_than_equal(encoder.encodeQueueSize, 0); + encoder.reset(); + assert_equals(encoder.encodeQueueSize, 0); +}, 'encodeQueueSize test'); + +const testOpusEncoderConfigs = [ + { + comment: 'Empty Opus config', + opus: {}, + }, + { + comment: 'Opus with frameDuration', + opus: {frameDuration: 2500}, + }, + { + comment: 'Opus with complexity', + opus: {complexity: 10}, + }, + { + comment: 'Opus with useinbandfec', + opus: { + packetlossperc: 15, + useinbandfec: true, + }, + }, + { + comment: 'Opus with usedtx', + opus: {usedtx: true}, + }, + { + comment: 'Opus mixed parameters', + opus: { + frameDuration: 40000, + complexity: 0, + packetlossperc: 10, + useinbandfec: true, + usedtx: true, + }, + } +]; + +testOpusEncoderConfigs.forEach(entry => { + promise_test(async t => { + let sample_rate = 48000; + let total_duration_s = 0.5; + let data_count = 10; + let outputs = []; + let init = { + error: e => { + assert_unreached('error: ' + e); + }, + output: chunk => { + outputs.push(chunk); + } + }; + + let encoder = new AudioEncoder(init); + + assert_equals(encoder.state, 'unconfigured'); + let config = { + codec: 'opus', + sampleRate: sample_rate, + numberOfChannels: 2, + bitrate: 256000, // 256kbit + opus: entry.opus, + }; + + encoder.configure(config); + + let timestamp_us = 0; + let data_duration_s = total_duration_s / data_count; + let data_length = data_duration_s * config.sampleRate; + for (let i = 0; i < data_count; i++) { + let data = make_audio_data( + timestamp_us, config.numberOfChannels, config.sampleRate, + data_length); + encoder.encode(data); + data.close(); + timestamp_us += data_duration_s * 1_000_000; + } + + // Encoders might output an extra buffer of silent padding. + let padding_us = data_duration_s * 1_000_000; + + await encoder.flush(); + encoder.close(); + assert_greater_than_equal(outputs.length, data_count); + assert_equals(outputs[0].timestamp, 0, 'first chunk timestamp'); + let total_encoded_duration = 0 + for (chunk of outputs) { + assert_greater_than(chunk.byteLength, 0, 'chunk byteLength'); + assert_greater_than_equal( + timestamp_us + padding_us, chunk.timestamp, 'chunk timestamp'); + assert_greater_than(chunk.duration, 0, 'chunk duration'); + total_encoded_duration += chunk.duration; + } + + // The total duration might be padded with silence. + assert_greater_than_equal( + total_encoded_duration, total_duration_s * 1_000_000); + }, 'Test encoding Opus with additional parameters: ' + entry.comment); +})
\ No newline at end of file |