blob: addeb1a238a01dea90ca42358c7cd2d249aab8a6 [file] [log] [blame]
<!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>