pcm player started

pull/14/head
Samir 7 years ago
parent 06c7189e0d
commit 6a56b51bbe

@ -1,49 +0,0 @@
import { appendByteArray } from './utils/utils.js';
import Event from './utils/event.js';
import Ogg from './utils/ogg.js';
import OpusWorker from './utils/opus-worker.js';
export class OpusToPCM extends Event {
constructor(options) {
super('decoder');
window.MediaSource = window.MediaSource || window.WebKitMediaSource;
let nativeSupport = !!(window.MediaSource && window.MediaSource.isTypeSupported('audio/webm; codecs=opus'));
let defaults = {
channels: 1,
fallback: true
};
options = Object.assign({}, defaults, options);
if (nativeSupport) {
this.decoder = new Ogg(options.channels);
} else if(options.fallback) {
this.decoder = new OpusWorker(options.channels);
} else {
this.decoder = null;
}
if (this.decoder) {
this.decoder.on('data', this.onData.bind(this));
}
}
getSampleRate() {
return this.decoder.getSampleRate();
}
onData(data) {
this.dispatch('decode', data);
}
decode(packet) {
if (!this.decoder) {
throw ('opps! no decoder is found to decode');
}
this.decoder.decode(packet);
}
destroy() {
this.decoder.destroy();
this.offAll();
}
}

@ -0,0 +1,71 @@
function PCMPlayer(channels, sampleRate) {
this.samples = new Float32Array();
this.flushingTime = 200;
this.channels = channels;
this.sampleRate = sampleRate;
this.createContext = function() {
this.audioCtx = new (window.AudioContext || window.webkitAudioContext)();
this.gainNode = this.audioCtx.createGain();
this.gainNode.gain.value = 1;
this.gainNode.connect(this.audioCtx.destination);
this.startTime = this.audioCtx.currentTime;
};
this.stopFlushing = function() {
if (this.interval) {
clearInterval(this.interval);
}
};
this.feed = function(data) {
let tmp = new Float32Array(this.samples.length + data.length);
tmp.set(this.samples, 0);
tmp.set(data, this.samples.length);
this.samples = tmp;
};
this.flush = function() {
if (!this.channels || !this.sampleRate || !this.samples.length) return;
let bufferSource = this.audioCtx.createBufferSource(),
length = this.samples.length,
audioBuffer = this.audioCtx.createBuffer(this.channels, length, this.sampleRate),
audioData,
channel,
offset,
i,
decrement = 50;
for (channel = 0; channel < this.channels; channel++) {
audioData = audioBuffer.getChannelData(channel);
offset = channel;
for (i = 0; i < length; i++) {
audioData[i] = this.samples[offset];
/* fadein */
if (i < 50) {
audioData[i] = (audioData[i] * i) / 50;
}
/* fadeout*/
if (i >= (length - 51)) {
audioData[i] = (audioData[i] * decrement--) / 50;
}
offset += this.channels;
}
}
if (this.startTime < this.audioCtx.currentTime) {
this.startTime = this.audioCtx.currentTime;
}
bufferSource.buffer = audioBuffer;
bufferSource.connect(this.gainNode);
bufferSource.start(this.startTime);
this.startTime += audioBuffer.duration;
this.samples = new Float32Array();
};
/* initiate start flushing */
this.flush = this.flush.bind(this);
this.createContext();
this.interval = setInterval(this.flush, this.flushingTime);
}

@ -1,39 +0,0 @@
export default class Event {
constructor(type) {
this.listener = {};
this.type = type | '';
}
on(event, fn) {
if (!this.listener[event]) {
this.listener[event] = [];
}
this.listener[event].push(fn);
return true;
}
off(event, fn) {
if (this.listener[event]) {
var index = this.listener[event].indexOf(fn);
if (index > -1) {
this.listener[event].splice(index, 1);
}
return true;
}
return false;
}
offAll() {
this.listener = {};
}
dispatch(event, data) {
if (this.listener[event]) {
this.listener[event].map((each) => {
each.apply(null, [data]);
});
return true;
}
return false;
}
}

@ -1,176 +0,0 @@
import Event from './event.js';
import { appendByteArray } from './utils.js';
export default class Ogg extends Event {
constructor(channel) {
super('ogg');
this.channel = channel;
this.audioCtx = new (window.AudioContext || window.webkitAudioContext)();
this.queue = [];
this.flushLimit = 20; /* the larger flush limit, the lesser clicking noise */
this.init();
}
getSampleRate() {
return this.audioCtx.sampleRate;
}
init() {
let header,
page;
this.oggHeader = new Uint8Array();
this.pageIndex = 0;
this.serial = Math.ceil(Math.random() * Math.pow(2,32));
this.initChecksumTable();
/* ID Header */
header = this.getIDHeader();
page = this.getPage(header, 2); // headerType of ID header is 2 i.e beginning of stream
this.oggHeader = appendByteArray(this.oggHeader, page);
/* comment Header */
header = this.getCommentHeader();
page = this.getPage(header, 0); // headerType of comment header is 0
this.oggHeader = appendByteArray(this.oggHeader, page);
}
getIDHeader() {
let data = new Uint8Array(19),
dv = new DataView(data.buffer);
dv.setUint32( 0, 1937076303, true ); // Magic Signature 'Opus'
dv.setUint32( 4, 1684104520, true ); // Magic Signature 'Head'
dv.setUint8( 8, 1, true ); // Version
dv.setUint8( 9, this.channel, true ); // Channel count
dv.setUint16( 10, 0, true ); // pre-skip, don't need to skip any value
dv.setUint32( 12, 8000, true ); // original sample rate, any valid sample e.g 8000
dv.setUint16( 16, 0, true ); // output gain
dv.setUint8( 18, 0, true ); // channel map 0 = one stream: mono or stereo
return data;
}
getCommentHeader() {
let data = new Uint8Array(20),
dv = new DataView(data.buffer);
dv.setUint32( 0, 1937076303, true ); // Magic Signature 'Opus'
dv.setUint32( 4, 1936154964, true ); // Magic Signature 'Tags'
dv.setUint32( 8, 4, true ); // Vendor Length
dv.setUint32( 12, 1633837924, true ); // Vendor name 'abcd'
dv.setUint32( 16, 0, true ); // User Comment List Length
return data;
}
getPage(segmentData, headerType) {
/* ref: https://tools.ietf.org/id/draft-ietf-codec-oggopus-00.html */
let segmentTable = new Uint8Array(1); /* segment table stores segment length map. always providing one single segment */
let page = new Uint8Array(27 + segmentTable.byteLength + segmentData.byteLength);
let pageDV = new DataView(page.buffer);
segmentTable[0] = segmentData.length;
pageDV.setUint32( 0, 1399285583, true); // page headers starts with 'OggS'
pageDV.setUint8( 4, 0, true ); // Version
pageDV.setUint8( 5, headerType, true ); // 1 = continuation, 2 = beginning of stream, 4 = end of stream
pageDV.setUint32( 6, -1, true ); // granuale position -1 i.e single packet per page. storing into bytes.
pageDV.setUint32( 10, -1, true );
pageDV.setUint32( 14, this.serial, true ); // Bitstream serial number
pageDV.setUint32( 18, this.pageIndex++, true ); // Page sequence number
pageDV.setUint8( 26, 1, true ); // Number of segments in page, giving always 1 segment
page.set( segmentTable, 27 ); // Segment Table inserting at 27th position since page header length is 27
page.set( segmentData, 28 ); // inserting at 28th since Segment Table(1) + header length(27)
pageDV.setUint32( 22, this.getChecksum( page ), true ); // Checksum - generating for page data and inserting at 22th position into 32 bits
return page;
}
getOGG() {
let oggData = this.oggHeader,
packet,
segmentData,
headerType;
while (this.queue.length) {
packet = this.queue.shift();
headerType = this.queue.length == 0 ? 4 : 0; // for last packet, header type should be end of stream
segmentData = this.getPage(packet, headerType);
oggData = appendByteArray(oggData, segmentData);
}
this.pageIndex = 2; /* reseting pageIndex to 2 so we can re-use same header */
return oggData;
}
getChecksum(data) {
let checksum = 0;
for ( var i = 0; i < data.length; i++ ) {
checksum = (checksum << 8) ^ this.checksumTable[ ((checksum>>>24) & 0xff) ^ data[i] ];
}
return checksum >>> 0;
}
initChecksumTable () {
this.checksumTable = [];
for ( var i = 0; i < 256; i++ ) {
var r = i << 24;
for ( var j = 0; j < 8; j++ ) {
r = ((r & 0x80000000) != 0) ? ((r << 1) ^ 0x04c11db7) : (r << 1);
}
this.checksumTable[i] = (r & 0xffffffff);
}
}
decode(packet) {
this.queue.push(packet);
if (this.queue.length >= this.flushLimit) {
this.process();
}
}
process() {
let ogg = this.getOGG();
this.audioCtx.decodeAudioData(ogg.buffer, (audioBuffer) => {
let pcmFloat;
if (this.channel == 1) {
pcmFloat = audioBuffer.getChannelData(0);
} else {
pcmFloat = this.getMergedPCMData(audioBuffer);
}
this.dispatch('data', pcmFloat);
});
}
getMergedPCMData(audioBuffer) {
let audioData,
result = [],
length,
pcmFloat,
offset = 0,
i=0,
j=0;
for(i=0; i<this.channel; i++) {
audioData = audioBuffer.getChannelData(i);
result.push(audioData);
}
length = result[0].length;
pcmFloat = new Float32Array(this.channel * length);
i = 0;
while(length > i) {
for(j=0; j<this.channel; j++) {
pcmFloat[offset++] = result[j][i];
}
i++;
}
return pcmFloat;
}
destroy() {
this.oggHeader = null;
this.audioCtx = null;
this.checksumTable = null;
this.offAll();
}
}

@ -1,36 +0,0 @@
import Event from './event.js';
export default class OpusWorker extends Event {
constructor(channels) {
super('worker');
this.worker = new Worker('libopus/opus.min.js');
this.worker.addEventListener('message', this.onMessage.bind(this));
this.worker.postMessage({
type: 'init',
config: {
rate:24000,
channels:channels
}
});
}
getSampleRate() {
return 24000;
}
decode(packet) {
let workerData = {
type: 'decode',
buffer: packet
};
this.worker.postMessage(workerData);
}
onMessage(event) {
let data = event.data;
this.dispatch('data', data.buffer);
}
destroy() {
this.worker = null;
this.offAll();
}
}

@ -1,6 +0,0 @@
export function appendByteArray(buffer1, buffer2) {
let tmp = new Uint8Array((buffer1.length|0) + (buffer2.length|0));
tmp.set(buffer1, 0);
tmp.set(buffer2, buffer1.length|0);
return tmp;
}
Loading…
Cancel
Save