| // 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); |
| }) |