| <!DOCTYPE html> |
| <!-- |
| Check that AudioDecoder can decode output of AudioEncoder |
| --> |
| <title>Encode test</title> |
| <script src="webcodecs_common.js"></script> |
| <script type="text/javascript"> |
| 'use strict'; |
| |
| function make_audio_data(timestamp, channels, sampleRate, frames) { |
| let data = new Float32Array(frames*channels); |
| |
| // This generates samples in a planar format. |
| for (var channel = 0; channel < channels; channel++) { |
| let hz = 100 + channel * 50; // sound frequency |
| let base_index = channel * frames; |
| for (var i = 0; i < frames; i++) { |
| let t = (i / sampleRate) * hz * (Math.PI * 2); |
| data[base_index + i] = Math.sin(t); |
| } |
| } |
| |
| return new AudioData({ |
| timestamp: timestamp, |
| data: data, |
| numberOfChannels: channels, |
| numberOfFrames: frames, |
| sampleRate: sampleRate, |
| format: "f32-planar", |
| }); |
| } |
| |
| async function main(args) { |
| let errors = 0; |
| let output_count = 0; |
| |
| let config = { |
| codec: args.codec, |
| sampleRate: args.sample_rate, |
| numberOfChannels: args.channels, |
| bitrate: 96000 |
| }; |
| |
| if (args.aac_format) { |
| config.aac = { |
| format : args.aac_format |
| }; |
| } |
| let decoder_config = null; |
| |
| let supported = false; |
| try { |
| supported = (await AudioEncoder.isConfigSupported(config)).supported; |
| } catch (e) {} |
| if (!supported) { |
| TEST.skip('Unsupported codec: ' + args.codec); |
| return; |
| } |
| |
| let total_duration_s = 1.0; |
| let data_count = 20; // each chunk is 500 ms |
| let outputs = []; |
| let init = { |
| error: e => { |
| errors++; |
| TEST.log(e); |
| }, |
| output: (chunk, md) => { |
| if (md.decoderConfig) { |
| decoder_config = md.decoderConfig; |
| } |
| outputs.push(chunk); |
| } |
| }; |
| |
| // Make some sin() waves and encode them |
| let encoder = new AudioEncoder(init); |
| 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; |
| if (i % 10 == 0) { |
| // Inserting flushes in the middle, to see how the encoder handles |
| // more inputs coming after flush() |
| await encoder.flush(); |
| } |
| |
| } |
| await encoder.flush(); |
| encoder.close(); |
| |
| // Maximum amount of padding from supported encoders. Applied twice, since the |
| // flush above can also introduce padding. |
| timestamp_us += 2 * (2112 / config.sampleRate) * 1_000_000; |
| |
| TEST.assert(decoder_config != null, "No decoder config"); |
| if (args.aac_format == "adts") { |
| TEST.assert(decoder_config.description == null, "ADTS should carry desc"); |
| } else if (args.aac_format == "aac") { |
| TEST.assert(decoder_config.description != null, "AAC should carry desc"); |
| TEST.assert(decoder_config.description.byteLength > 1, |
| "AAC desc is too short"); |
| } |
| TEST.assert(outputs.length > 0, "no outputs"); |
| TEST.assert(outputs[0].timestamp == 0, "first chunk timestamp non zero"); |
| |
| let total_encoded_duration = 0 |
| for (let chunk of outputs) { |
| TEST.assert(chunk.byteLength > 0, "chunk is empty"); |
| TEST.assert(timestamp_us >= chunk.timestamp, |
| `chunk timestamp is too small. ${timestamp_us} vs ${chunk.timestamp}`); |
| TEST.assert(chunk.duration >= 0, "chunk duration is zero"); |
| total_encoded_duration += chunk.duration; |
| let buf = new ArrayBuffer(chunk.byteLength); |
| chunk.copyTo(buf); |
| if (args.aac_format == "adts") { |
| let adts_header = new DataView(buf).getUint8(0); |
| TEST.assert(adts_header == 0xff, "Incorrect ADTS header"); |
| } |
| } |
| |
| // The total duration might be padded with silence. |
| TEST.assert( |
| total_encoded_duration >= total_duration_s * 1_000_000, |
| `Unexpected encoded duration: ${total_encoded_duration} us` ); |
| |
| |
| // Decode output and check that the output still makes sense |
| let audio_buffers = []; |
| let decoder_init = { |
| error: e => { |
| errors++; |
| TEST.log(e); |
| }, |
| output: (audio_data) => { |
| let buffer = new AudioBuffer({ |
| length: audio_data.numberOfFrames, |
| numberOfChannels: audio_data.numberOfChannels, |
| sampleRate: audio_data.sampleRate |
| }); |
| for (let i = 0; i < audio_data.numberOfChannels; i++) { |
| audio_data.copyTo(buffer.getChannelData(i), { |
| planeIndex : i, |
| frameOffset : 0, |
| frameCount : audio_data.numberOfFrames, |
| format : "f32-planar" |
| }); |
| } |
| audio_buffers.push(buffer); |
| audio_data.close(); |
| } |
| }; |
| |
| let decoder = new AudioDecoder(decoder_init) |
| decoder.configure(decoder_config); |
| for (let chunk of outputs) { |
| decoder.decode(chunk); |
| } |
| await decoder.flush(); |
| |
| let total_decoded_duration_s = 0.0; |
| for (let buffer of audio_buffers) { |
| total_decoded_duration_s += buffer.duration; |
| TEST.assert(buffer.numberOfChannels == config.numberOfChannels, |
| "unexpected number of channels"); |
| } |
| |
| TEST.assert(errors == 0, "errors: " + errors); |
| |
| // The total duration might be padded with silence, but no too much. |
| TEST.assert( |
| total_decoded_duration_s >= total_duration_s, |
| `Decoded duration is too short: ${total_decoded_duration_s} s`); |
| TEST.assert( |
| total_decoded_duration_s < 2 * total_duration_s, |
| `Decoded duration is too long: ${total_decoded_duration_s} s`); |
| } |
| </script> |