diff --git a/src/opus-to-pcm.js b/src/opus-to-pcm.js deleted file mode 100644 index 5dfd5a3..0000000 --- a/src/opus-to-pcm.js +++ /dev/null @@ -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(); - } -} diff --git a/src/pcm-player.js b/src/pcm-player.js new file mode 100644 index 0000000..47bdd72 --- /dev/null +++ b/src/pcm-player.js @@ -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); +} \ No newline at end of file diff --git a/src/utils/event.js b/src/utils/event.js deleted file mode 100644 index d651ef4..0000000 --- a/src/utils/event.js +++ /dev/null @@ -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; - } -} diff --git a/src/utils/ogg.js b/src/utils/ogg.js deleted file mode 100644 index 189dc28..0000000 --- a/src/utils/ogg.js +++ /dev/null @@ -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 i) { - for(j=0; j