pcm player started
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…
Reference in New Issue