| <!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) { |
| for (var i = 0; i < string.length; i++) |
| array.set([string.charCodeAt(i)], i + offset); |
| return string.length; |
| } |
| function writeValue(array, bytes, offset, value) { |
| for (var i = 0; i < bytes; i++) |
| array.set([((value >>> (i * 8)) & 0xff)], offset + i); |
| return bytes; |
| } |
| |
| var BufferArray = function() { |
| this._arrays = [ this._createBuffer() ]; |
| }; |
| BufferArray.prototype = { |
| _createBuffer: function() { |
| var buffer = new Uint8Array(100 * 1024); |
| buffer.dataLength = 0; |
| return buffer; |
| }, |
| appendData: function(data) { |
| var currentBuffer = this._arrays[this._arrays.length - 1]; |
| if (currentBuffer.byteLength < (currentBuffer.dataLength + data.byteLength)) { |
| this._arrays.push(this._createBuffer()); |
| currentBuffer = this._arrays[this._arrays.length - 1]; |
| } |
| currentBuffer.set(new Uint8Array(data), currentBuffer.dataLength); |
| currentBuffer.dataLength += data.byteLength; |
| }, |
| getByte: function(offset) { |
| var bufferOffset = 0; |
| for (var i = 0; i < this._arrays.length; i++) { |
| var buffer = this._arrays[i]; |
| if (offset < (bufferOffset + buffer.dataLength)) { |
| return buffer[offset - bufferOffset]; |
| } |
| bufferOffset += buffer.dataLength; |
| } |
| throw new Error('Out of range access'); |
| }, |
| getSize: function() { |
| var size = 0; |
| for (var i = 0; i < this._arrays.length; i++) |
| size += this._arrays[i].dataLength; |
| return size; |
| }, |
| writeValue: function(bytes, offset, value) { |
| var bufferOffset = 0; |
| for (var i = 0; i < this._arrays.length; i++) { |
| var buffer = this._arrays[i]; |
| if (offset < (bufferOffset + buffer.dataLength)) { |
| writeValue(buffer, bytes, offset - bufferOffset, value); |
| return; |
| } |
| bufferOffset += buffer.dataLength; |
| } |
| throw new Error('Out of range access'); |
| }, |
| toBlob: function() { |
| var result = new Uint8Array(this.getSize()); |
| var offset = 0; |
| for (var i = 0; i < this._arrays.length; i++) { |
| var buffer = this._arrays[i]; |
| result.set(buffer.subarray(0, buffer.dataLength), offset); |
| offset += buffer.dataLength; |
| } |
| return new Blob([result]); |
| }, |
| }; |
| |
| // Writes wav data. |
| var WavWriter = function() { |
| this.chunkSizeOffset = 0; |
| this.buffer = new BufferArray(); |
| }; |
| WavWriter.prototype = { |
| writeHeader: function(format) { |
| var buffer = |
| new Uint8Array(4 + 4 + 4 + 4 + 4 + 2 + 2 + 4 + 4 + 2 + 2 + 4 + 4); |
| var i = 0; |
| // File header |
| i += writeString(buffer, i, 'RIFF'); |
| i += 4; // Gap for final size. |
| i += writeString(buffer, i, 'WAVE'); |
| // Chunk ID. |
| i += writeString(buffer, i, 'fmt '); |
| // Chunk length. |
| i += writeValue(buffer, 4, i, 16); |
| // Codec (uncompressed LPCM). |
| i += writeValue(buffer, 2, i, 1); |
| // Number of channels. |
| i += writeValue(buffer, 2, i, format.channels); |
| // Sample rate. |
| i += writeValue(buffer, 4, i, format.sample_rate); |
| // Average bytes per seconds (sample rate * bytes per sample) |
| i += writeValue(buffer, 4, i, |
| format.sample_rate * format.sample_size); |
| // Bytes per sample. |
| i += writeValue(buffer, 2, i, |
| format.sample_size * format.channels); |
| // Bits per sample. |
| i += writeValue(buffer, 2, i, format.sample_size * 8); |
| |
| // Data chunk |
| i += writeString(buffer, i, 'data'); |
| this.chunkSizeOffset = i; // Location of the chunk's size |
| this.buffer.appendData(buffer); |
| }, |
| writeData: function(data) { |
| this.buffer.appendData(data); |
| }, |
| end: function() { |
| this.buffer.writeValue(4, 4, this.buffer.getSize() - 8); |
| this.buffer.writeValue(4, this.chunkSizeOffset, |
| this.buffer.getSize() - this.chunkSizeOffset); |
| }, |
| getSize: function() { |
| return this.buffer.getSize(); |
| }, |
| getData: function() { |
| return this.buffer; |
| }, |
| getExtension: function() { |
| return 'wav'; |
| }, |
| }; |
| |
| // Writes ogg data. |
| var OggWriter = function(profile) { |
| this.writeHeader = this._writeOpusHeader; |
| this.buffer = new BufferArray(); |
| this.pageSequence = 0; |
| this.bitstreamNumber = 0; |
| this.position = 0; |
| this.dataWritten = false; |
| }; |
| OggWriter.prototype = { |
| _Start: 0x2, |
| _Continue: 0x1, |
| _Stop: 0x4, |
| _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 crcTable = this._crcTable || (this._crcTable = this._makeCRCTable()); |
| for (var i = start; i < end; i++) |
| crc = (crc << 8) ^ crcTable[((crc >> 24) & 0xff) ^ data.getByte(i)]; |
| return crc; |
| }, |
| _writePage: function(flag, size, position) { |
| var pages = 1 + Math.floor(size / 255); |
| var buffer = new Uint8Array(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.buffer.appendData(buffer); |
| }, |
| _writePageChecksum: function(pageOffset) { |
| var crc = this._crc32(this.buffer, pageOffset, |
| this.buffer.getSize()); |
| this.buffer.writeValue(4, pageOffset + 22, crc); |
| }, |
| _writeOpusHeader: function(format) { |
| this.format = format; |
| var start = this.buffer.getSize(); |
| var buffer = new Uint8Array(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.buffer.appendData(buffer); |
| this._writePageChecksum(start); |
| this._writeCommentHeader(); |
| }, |
| _writeCommentHeader: function(name) { |
| var start = this.buffer.getSize(); |
| var buffer = new Uint8Array(8 + 4 + 8 + 4 + 4 + 13), i = 0; |
| // Opus comment header. |
| i += writeString(buffer, i, 'OpusTags'); |
| // 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(0, buffer.byteLength); |
| this.buffer.appendData(buffer); |
| this._writePageChecksum(start); |
| }, |
| _writeLengthString: function(buffer, offset, str) { |
| return (writeValue(buffer, 4, offset, str.length) + |
| writeString(buffer, offset + 4, str)); |
| }, |
| writeData: function(data) { |
| this.position += this.format.sample_per_frame / this.format.channels; |
| var start = this.buffer.getSize(); |
| this._writePage(0, data.byteLength, this.position); |
| this.buffer.appendData(data); |
| this._writePageChecksum(start); |
| this.dataWritten = true; |
| }, |
| end: function() { |
| this._writePage(this._Stop, 0); |
| }, |
| getData: function() { |
| return this.buffer; |
| }, |
| 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(blob, filename) { |
| var mimeType = 'application/octet-stream'; |
| 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().toBlob(), |
| 'Capture.' + writer.getExtension()); |
| } |
| |
| 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.getData().getSize() + ' bytes'; |
| } else if (msg.data.command == 'format') { |
| writer.writeHeader(msg.data); |
| $('length').textContent = ' Size: ' + writer.getData().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.getData().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> |