| Index: ppapi/examples/audio_encode/audio_encode.html
|
| diff --git a/ppapi/examples/audio_encode/audio_encode.html b/ppapi/examples/audio_encode/audio_encode.html
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..36b322a54d1e8d236a68a4d86867a3bd94a477c0
|
| --- /dev/null
|
| +++ b/ppapi/examples/audio_encode/audio_encode.html
|
| @@ -0,0 +1,378 @@
|
| +<!DOCTYPE html>
|
| +<html>
|
| + <!--
|
| + Copyright 2015 The Chromium Authors. All rights reserved.
|
| + Use of this source code is governed by a BSD-style license that can be
|
| + found in the LICENSE file.
|
| + -->
|
| +<head>
|
| + <title>Audio Encoder Example</title>
|
| + <script type="text/javascript">
|
| + var plugin;
|
| + var track;
|
| + var writer;
|
| + var profiles;
|
| +
|
| + function writeString(array, offset, string) {
|
| + var out = new Uint8Array(array);
|
| + for (var i = 0; i < string.length; i++)
|
| + out.set([string.charCodeAt(i)], i + offset);
|
| + return string.length;
|
| + }
|
| + function writeValue(array, bytes, offset, value) {
|
| + var out = new Uint8Array(array);
|
| + for (var i = 0; i < bytes; i++)
|
| + out.set([((value >>> (i * 8)) & 0xff)], offset + i);
|
| + return bytes;
|
| + }
|
| + // Writes way data.
|
| + var WavWriter = function() {
|
| + this.chunkSizeOffset = 0;
|
| + this.arrayBuffer = new ArrayBuffer();
|
| + };
|
| + WavWriter.prototype = {
|
| + writeHeader: function(format) {
|
| + this.arrayBuffer =
|
| + new ArrayBuffer(4 + 4 + 4 + 4 + 4 + 2 + 2 + 4 + 4 + 2 + 2 + 4 + 4);
|
| + var i = 0;
|
| + // File header
|
| + i += writeString(this.arrayBuffer, i, 'RIFF');
|
| + i += 4; // Gap for final size.
|
| + i += writeString(this.arrayBuffer, i, 'WAVE');
|
| + // Chunk ID.
|
| + i += writeString(this.arrayBuffer, i, 'fmt ');
|
| + // Chunk length.
|
| + i += writeValue(this.arrayBuffer, 4, i, 16);
|
| + // Codec (uncompressed LPCM).
|
| + i += writeValue(this.arrayBuffer, 2, i, 1);
|
| + // Number of channels.
|
| + i += writeValue(this.arrayBuffer, 2, i, format.channels);
|
| + // Sample rate.
|
| + i += writeValue(this.arrayBuffer, 4, i, format.sample_rate);
|
| + // Average bytes per seconds (sample rate * bytes per sample)
|
| + i += writeValue(this.arrayBuffer, 4, i,
|
| + format.sample_rate * format.sample_size);
|
| + // Bytes per sample.
|
| + i += writeValue(this.arrayBuffer, 2, i,
|
| + format.sample_size * format.channels);
|
| + // Bits per sample.
|
| + i += writeValue(this.arrayBuffer, 2, i, format.sample_size * 8);
|
| +
|
| + // Data chunk
|
| + i += writeString(this.arrayBuffer, i, 'data');
|
| + this.chunkSizeOffset = i; // Location of the chunk's size
|
| + },
|
| + writeData: function(data) {
|
| + var tmp = new Uint8Array(this.arrayBuffer.byteLength + data.byteLength);
|
| + tmp.set(new Uint8Array(this.arrayBuffer), 0);
|
| + tmp.set(new Uint8Array(data), this.arrayBuffer.byteLength);
|
| + this.arrayBuffer = tmp.buffer;
|
| + },
|
| + end: function() {
|
| + var out = new Uint32Array(this.arrayBuffer);
|
| + out.set([this.arrayBuffer.byteLength - 8], 1);
|
| + out.set([this.arrayBuffer.byteLength - this.chunkSizeOffset],
|
| + this.chunkSizeOffset / 4);
|
| + },
|
| + getSize: function() {
|
| + return this.arrayBuffer.byteLength;
|
| + },
|
| + getData: function() {
|
| + return this.arrayBuffer;
|
| + },
|
| + getExtension: function() {
|
| + return 'wav';
|
| + },
|
| + };
|
| +
|
| + // Writes ogg data.
|
| + var OggWriter = function(profile) {
|
| + this.writeHeader = this._writeOpusHeader;
|
| + this.arrayBuffer = new ArrayBuffer();
|
| + this.pageSequence = 0;
|
| + this.bitstreamNumber = 0;
|
| + this.position = 0;
|
| + this.dataWritten = false;
|
| + };
|
| + OggWriter.prototype = {
|
| + _Start: 0x2,
|
| + _Continue: 0x1,
|
| + _Stop: 0x4,
|
| + _append: function(data) {
|
| + var tmp = new Uint8Array(this.arrayBuffer.byteLength + data.byteLength);
|
| + tmp.set(new Uint8Array(this.arrayBuffer), 0);
|
| + tmp.set(new Uint8Array(data), this.arrayBuffer.byteLength);
|
| + this.arrayBuffer = tmp.buffer;
|
| + },
|
| + _makeCRCTable: function() {
|
| + var crcTable = [];
|
| + for (var n = 0; n < 256; n++) {
|
| + var r = n << 24;
|
| + for (var i = 0; i < 8; i++) {
|
| + if (r & 0x80000000)
|
| + r = (r << 1) ^ 0x04c11db7;
|
| + else
|
| + r <<= 1;
|
| + }
|
| + crcTable[n] = r & 0xffffffff;
|
| + }
|
| + return crcTable;
|
| + },
|
| + _crc32: function(data, start, end) {
|
| + var crc = 0;
|
| + var u8data = new Uint8Array(data)
|
| + var crcTable = this._crcTable || (this._crcTable = this._makeCRCTable());
|
| + for (var i = start; i < end; i++)
|
| + crc = (crc << 8) ^ crcTable[((crc >> 24) & 0xff) ^ u8data[i]];
|
| + return crc;
|
| + },
|
| + _writePage: function(flag, size, position) {
|
| + var pages = 1 + Math.floor(size / 255);
|
| + var buffer = new ArrayBuffer(27 + pages), i = 0;
|
| + // capture_pattern.
|
| + i += writeString(buffer, i, 'OggS');
|
| + // stream_structure_version.
|
| + i += writeValue(buffer, 1, i, 0);
|
| + // header_type_flag.
|
| + i += writeValue(buffer, 1, i, flag);
|
| + // granule_position.
|
| + // TODO(llandwerlin): Not writing more than 32bits for now,
|
| + // because Javascript doesn't have 64bits integers, this limits
|
| + // the duration to ~24 hours at 48kHz sampling rate.
|
| + i += writeValue(buffer, 4, i, position != undefined ? position : 0);
|
| + i += writeValue(buffer, 4, i, 0);
|
| + // bitstream_serial_number.
|
| + i += writeValue(buffer, 4, i, this.bitstreamNumber);
|
| + // page_sequence_number.
|
| + i += writeValue(buffer, 4, i, this.pageSequence++);
|
| + // CRC_checksum.
|
| + i += writeValue(buffer, 4, i, 0);
|
| + // number_page_segments.
|
| + i += writeValue(buffer, 1, i, pages);
|
| + // segment sizes.
|
| + for (var j = 0; j < (pages - 1); j++)
|
| + i += writeValue(buffer, 1, i, 255);
|
| + i += writeValue(buffer, 1, i, size % 255);
|
| +
|
| + this._append(buffer);
|
| + },
|
| + _writePageChecksum: function(pageOffset) {
|
| + var crc = this._crc32(this.arrayBuffer, pageOffset,
|
| + this.arrayBuffer.byteLength);
|
| + writeValue(this.arrayBuffer, 4, pageOffset + 22, crc);
|
| + },
|
| + _writeOpusHeader: function(format) {
|
| + this.format = format;
|
| + var start = this.getSize();
|
| + var buffer = new ArrayBuffer(8 + 1 + 1 + 2 + 4 + 2 + 1), i = 0;
|
| + // Opus header.
|
| + i += writeString(buffer, i, 'OpusHead');
|
| + // version.
|
| + i += writeValue(buffer, 1, i, 1);
|
| + // channel count.
|
| + i += writeValue(buffer, 1, i, format.channels);
|
| + // pre-skip.
|
| + i += writeValue(buffer, 2, i, 0);
|
| + // input sample rate.
|
| + i += writeValue(buffer, 4, i, format.sample_rate);
|
| + // output gain.
|
| + i += writeValue(buffer, 2, i, 0);
|
| + // channel mapping family.
|
| + i += writeValue(buffer, 1, i, 0);
|
| +
|
| + this._writePage(this._Start, buffer.byteLength);
|
| + this._append(buffer);
|
| + this._writePageChecksum(start);
|
| + this._writeCommentHeader('OpusTags');
|
| + },
|
| + _writeCommentHeader: function(name) {
|
| + var start = this.getSize();
|
| + var buffer = new ArrayBuffer(8 + 4 + 8 + 4 + 4 + 13), i = 0;
|
| + // Opus comment header.
|
| + i += writeString(buffer, i, name);
|
| + // Vendor string.
|
| + i += this._writeLengthString(buffer, i, 'Chromium');
|
| + // User comment list length
|
| + i += writeValue(buffer, 4, i, 1);
|
| + // User comment 0 length.
|
| + i += this._writeLengthString(buffer, i, 'TITLE=example');
|
| +
|
| + this._writePage(this._Continue, buffer.byteLength);
|
| + this._append(buffer);
|
| + this._writePageChecksum(start);
|
| + },
|
| + _writeLengthString: function(buffer, offset, str) {
|
| + return (writeValue(buffer, offset, 4, str.length) +
|
| + writeString(buffer, offset, str));
|
| + },
|
| + writeData: function(data) {
|
| + this.position += this.format.sample_per_frame / this.format.channels;
|
| + var start = this.getSize();
|
| + this._writePage(0, data.byteLength, this.position);
|
| + this._append(data);
|
| + this._writePageChecksum(start);
|
| + this.dataWritten = true;
|
| + },
|
| + end: function() {
|
| + this._writePage(this._Stop, 0);
|
| + },
|
| + getSize: function() {
|
| + return this.arrayBuffer.byteLength;
|
| + },
|
| + getData: function() {
|
| + return this.arrayBuffer;
|
| + },
|
| + getExtension: function() {
|
| + return 'ogg';
|
| + },
|
| + };
|
| +
|
| + function $(id) {
|
| + return document.getElementById(id);
|
| + }
|
| +
|
| + function success(stream) {
|
| + track = stream.getAudioTracks()[0];
|
| + var list = $('profileList');
|
| + var profile = profiles[list.selectedIndex];
|
| + plugin.postMessage({
|
| + command: 'start',
|
| + profile: profile.name,
|
| + sample_size: profile.sample_size,
|
| + sample_rate: profile.sample_rate,
|
| + track: track,
|
| + });
|
| + }
|
| +
|
| + function failure(e) {
|
| + console.log("Error: ", e);
|
| + }
|
| +
|
| + function cleanupDownload() {
|
| + var download = $('download');
|
| + if (!download)
|
| + return;
|
| + download.parentNode.removeChild(download);
|
| + }
|
| +
|
| + function setDownload(data, filename) {
|
| + var mimeType = 'application/octet-stream';
|
| + var blob = new Blob([data], { type: mimeType });
|
| + var a = document.createElement('a');
|
| + a.id = "download";
|
| + a.download = filename;
|
| + a.href = window.URL.createObjectURL(blob);
|
| + a.textContent = 'Download';
|
| + a.dataset.downloadurl = [mimeType, a.download, a.href].join(':');
|
| + $('download-box').appendChild(a);
|
| + }
|
| +
|
| + function startRecord() {
|
| + var list = $('profileList');
|
| + var profile = profiles[list.selectedIndex];
|
| + if (profile.name == 'wav')
|
| + writer = new WavWriter();
|
| + else
|
| + writer = new OggWriter(profile);
|
| + cleanupDownload();
|
| +
|
| + var constraints = [];
|
| + if (profile.name == 'opus') {
|
| + // Chromium outputs 32kHz sampling rate by default. This isn't a
|
| + // supported sampling rate for the Opus codec. So we force the
|
| + // output to 48kHz. If Chromium implements the GetUserMedia
|
| + // audio constraints at some point, we could potentially get rid
|
| + // of this.
|
| + constraints.push({ googAudioProcessing48kHzSupport: true });
|
| + }
|
| +
|
| + navigator.webkitGetUserMedia({ audio: { optional: constraints },
|
| + video: false},
|
| + success, failure);
|
| + }
|
| +
|
| + function stopRecord() {
|
| + plugin.postMessage({
|
| + command: "stop"
|
| + });
|
| + track.stop();
|
| + writer.end();
|
| + setDownload(writer.getData(), 'Capture.' + writer.getExtension());
|
| + }
|
| +
|
| + function saveBlob(blob) {
|
| + var blobUrl = URL.createObjectURL(blob);
|
| + window.location = blobUrl;
|
| + }
|
| +
|
| + function handleMessage(msg) {
|
| + if (msg.data.command == 'log') {
|
| + console.log(msg.data.message);
|
| + } else if (msg.data.command == 'error') {
|
| + console.error(msg.data.message);
|
| + } else if (msg.data.command == 'data') {
|
| + writer.writeData(msg.data.buffer);
|
| + $('length').textContent = ' Size: ' + writer.getSize() + ' bytes';
|
| + } else if (msg.data.command == 'format') {
|
| + writer.writeHeader(msg.data);
|
| + $('length').textContent = ' Size: ' + writer.getSize() + ' bytes';
|
| + } else if (msg.data.command == 'supportedProfiles') {
|
| + profiles = [];
|
| + var profileList = $('profileList');
|
| + while (profileList.lastChild)
|
| + profileList.remove(profileList.lastChild);
|
| +
|
| + var item = document.createElement('option');
|
| + item.label = 'wav';
|
| + profiles.push({ name: 'wav',
|
| + sample_rate: 0,
|
| + sample_size: 0,
|
| + sample_per_frame:
|
| + msg.data.profiles[0].sample_per_frame });
|
| + profileList.appendChild(item);
|
| + for (var i = 0; i < msg.data.profiles.length; i++) {
|
| + var item = document.createElement('option');
|
| + item.label = msg.data.profiles[i].name + ' - ' +
|
| + msg.data.profiles[i].sample_rate + 'Hz';
|
| + profiles.push(msg.data.profiles[i]);
|
| + profileList.appendChild(item);
|
| + }
|
| + }
|
| + }
|
| +
|
| + function resetData() {
|
| + writer = new WavWriter();
|
| + $('length').textContent = ' Size: ' + writer.getSize() + ' bytes';
|
| + }
|
| +
|
| + function initialize() {
|
| + plugin = $('plugin');
|
| + plugin.addEventListener('message', handleMessage, false);
|
| +
|
| + $('start').addEventListener('click', function (e) {
|
| + resetData();
|
| + startRecord();
|
| + });
|
| + $('stop').addEventListener('click', function (e) {
|
| + stopRecord();
|
| + });
|
| + }
|
| +
|
| + document.addEventListener('DOMContentLoaded', initialize, false);
|
| + </script>
|
| +</head>
|
| +
|
| +<body>
|
| + <h1>Pepper Audio Encoder API Example</h1><br>
|
| + This example demonstrates receiving frames from an audio MediaStreamTrack and
|
| + encoding them using AudioEncode.<br>
|
| +
|
| + <select id="profileList"></select>
|
| + <input type="button" id="start" value="Start Recording"/>
|
| + <input type="button" id="stop" value="Stop Recording"/>
|
| + <div id="download-box"></div>
|
| + <div id="length"></div>
|
| + <br>
|
| + <embed id="plugin" type="application/x-ppapi-example-audio-encode"/>
|
| +</body>
|
| +</html>
|
|
|