mirror of
https://github.com/servo/servo.git
synced 2025-06-26 01:54:33 +01:00
572 lines
No EOL
16 KiB
JavaScript
572 lines
No EOL
16 KiB
JavaScript
// 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);
|
|
}) |