chromium/content/test/data/gpu/webcodecs/audio-encode-decode.html

<!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>