UNPKG

opus-encdec

Version:

A JavaScript library for encoding and decoding OPUS audio

330 lines (261 loc) 9.04 kB
"use strict"; var AudioContext = global.AudioContext || global.webkitAudioContext; // Constructor var Recorder = function( config = {} ){ if ( !Recorder.isRecordingSupported() ) { throw new Error("Recording is not supported in this browser"); } this.state = "inactive"; this.config = Object.assign({ bufferLength: 4096, encoderApplication: 2049, encoderFrameSize: 20, encoderPath: 'encoderWorker.min.js', encoderSampleRate: 48000, maxFramesPerPage: 40, mediaTrackConstraints: true, monitorGain: 0, numberOfChannels: 1, recordingGain: 1, resampleQuality: 3, streamPages: false, wavBitDepth: 16, sourceNode: { context: null }, }, config ); this.encodedSamplePosition = 0; this.initAudioContext(); this.initialize = this.initWorklet().then(() => this.initEncoder()); }; // Static Methods Recorder.isRecordingSupported = function(){ const getUserMediaSupported = global.navigator && global.navigator.mediaDevices && global.navigator.mediaDevices.getUserMedia; return AudioContext && getUserMediaSupported && global.WebAssembly; }; Recorder.version = '0.1.1'; // Instance Methods Recorder.prototype.clearStream = function(){ if ( this.stream ){ if ( this.stream.getTracks ) { this.stream.getTracks().forEach(track => track.stop()); } else { this.stream.stop(); } } }; Recorder.prototype.close = function() { this.monitorGainNode.disconnect(); this.recordingGainNode.disconnect(); if (this.sourceNode) { this.sourceNode.disconnect(); } this.clearStream(); if (this.encoder) { this.encoderNode.disconnect(); this.encoder.postMessage({ command: "close" }); } if ( !this.config.sourceNode.context ){ return this.audioContext.close(); } return Promise.resolve(); } Recorder.prototype.encodeBuffers = function( inputBuffer ){ if ( this.state === "recording" ) { var buffers = []; for ( var i = 0; i < inputBuffer.numberOfChannels; i++ ) { buffers[i] = inputBuffer.getChannelData(i); } this.encoder.postMessage({ command: "encode", buffers: buffers }); } }; Recorder.prototype.initAudioContext = function(){ this.audioContext = this.config.sourceNode.context ? this.config.sourceNode.context : new AudioContext(); this.monitorGainNode = this.audioContext.createGain(); this.setMonitorGain( this.config.monitorGain ); this.recordingGainNode = this.audioContext.createGain(); this.setRecordingGain( this.config.recordingGain ); }; Recorder.prototype.initEncoder = function() { if (this.audioContext.audioWorklet) { this.encoderNode = new AudioWorkletNode(this.audioContext, 'encoder-worklet', { numberOfOutputs: 0 }); this.encoder = this.encoderNode.port; } else { console.log('audioWorklet support not detected. Falling back to scriptProcessor'); // Skip the first buffer this.encodeBuffers = () => delete this.encodeBuffers; this.encoderNode = this.audioContext.createScriptProcessor( this.config.bufferLength, this.config.numberOfChannels, this.config.numberOfChannels ); this.encoderNode.onaudioprocess = ({ inputBuffer }) => this.encodeBuffers( inputBuffer ); this.encoderNode.connect( this.audioContext.destination ); // Requires connection to destination to process audio this.encoder = new global.Worker(this.config.encoderPath); } }; Recorder.prototype.initSourceNode = function(){ if ( this.config.sourceNode.context ) { this.sourceNode = this.config.sourceNode; return Promise.resolve(); } return global.navigator.mediaDevices.getUserMedia({ audio : this.config.mediaTrackConstraints }).then( stream => { this.stream = stream; this.sourceNode = this.audioContext.createMediaStreamSource( stream ); }); }; Recorder.prototype.initWorker = function(){ var onPage = (this.config.streamPages ? this.streamPage : this.storePage).bind(this); this.recordedPages = []; this.totalLength = 0; return new Promise(resolve => { var callback = ({ data }) => { switch( data['message'] ){ case 'ready': resolve(); break; case 'page': this.encodedSamplePosition = data['samplePosition']; onPage(data['page']); break; case 'done': this.encoder.removeEventListener( "message", callback ); this.finish(); break; } }; this.encoder.addEventListener( "message", callback ); // must call start for messagePort messages if( this.encoder.start ) { this.encoder.start() } // exclude sourceNode const {sourceNode, ...config} = this.config; this.encoder.postMessage( Object.assign({ command: 'init', originalSampleRate: this.audioContext.sampleRate, wavSampleRate: this.audioContext.sampleRate }, config)); }); }; Recorder.prototype.initWorklet = function() { if (this.audioContext.audioWorklet) { return this.audioContext.audioWorklet.addModule(this.config.encoderPath); } return Promise.resolve(); } Recorder.prototype.pause = function( flush ) { if ( this.state === "recording" ) { this.state = "paused"; this.recordingGainNode.disconnect(); if ( flush && this.config.streamPages ) { return new Promise(resolve => { var callback = ({ data }) => { if ( data["message"] === 'flushed' ) { this.encoder.removeEventListener( "message", callback ); this.onpause(); resolve(); } }; this.encoder.addEventListener( "message", callback ); // must call start for messagePort messages if ( this.encoder.start ) { this.encoder.start() } this.encoder.postMessage( { command: "flush" } ); }); } this.onpause(); return Promise.resolve(); } }; Recorder.prototype.resume = function() { if ( this.state === "paused" ) { this.state = "recording"; this.recordingGainNode.connect(this.encoderNode); this.onresume(); } }; Recorder.prototype.setRecordingGain = function( gain ){ this.config.recordingGain = gain; if ( this.recordingGainNode && this.audioContext ) { this.recordingGainNode.gain.setTargetAtTime(gain, this.audioContext.currentTime, 0.01); } }; Recorder.prototype.setMonitorGain = function( gain ){ this.config.monitorGain = gain; if ( this.monitorGainNode && this.audioContext ) { this.monitorGainNode.gain.setTargetAtTime(gain, this.audioContext.currentTime, 0.01); } }; Recorder.prototype.start = function(){ if ( this.state === "inactive" ) { this.state = 'loading'; this.encodedSamplePosition = 0; return this.audioContext.resume() .then(() => this.initialize) .then(() => Promise.all([this.initSourceNode(), this.initWorker()])) .then(() => { this.state = "recording"; this.encoder.postMessage({ command: 'getHeaderPages' }); this.sourceNode.connect( this.monitorGainNode ); this.sourceNode.connect( this.recordingGainNode ); this.monitorGainNode.connect( this.audioContext.destination ); this.recordingGainNode.connect( this.encoderNode ); this.onstart(); }) .catch(error => { this.state = 'inactive'; throw error; }); } return Promise.resolve(); }; Recorder.prototype.stop = function(){ if ( this.state === "paused" || this.state === "recording" ) { this.state = "inactive"; // macOS and iOS requires the source to remain connected (in case stopped while paused) this.recordingGainNode.connect( this.encoderNode ); this.monitorGainNode.disconnect(); this.clearStream(); return new Promise(resolve => { var callback = ({ data }) => { if ( data["message"] === 'done' ) { this.encoder.removeEventListener( "message", callback ); resolve(); } }; this.encoder.addEventListener( "message", callback ); // must call start for messagePort messages if( this.encoder.start ) { this.encoder.start() } this.encoder.postMessage({ command: "done" }); }); } return Promise.resolve(); }; Recorder.prototype.storePage = function( page ) { this.recordedPages.push( page ); this.totalLength += page.length; }; Recorder.prototype.streamPage = function( page ) { this.ondataavailable( page ); }; Recorder.prototype.finish = function() { if( !this.config.streamPages ) { var outputData = new Uint8Array( this.totalLength ); this.recordedPages.reduce( function( offset, page ){ outputData.set( page, offset ); return offset + page.length; }, 0); this.ondataavailable( outputData ); } this.onstop(); }; // Callback Handlers Recorder.prototype.ondataavailable = function(){}; Recorder.prototype.onpause = function(){}; Recorder.prototype.onresume = function(){}; Recorder.prototype.onstart = function(){}; Recorder.prototype.onstop = function(){}; module.exports = Recorder;