export default class AudioManager{ static get BUFFER_SIZE(){ return 2048; } constructor(){ /* (1) Build Audio Context */ this.ctx = new (window.AudioContext || window.webkitAudioContext)(); this.gain = this.ctx.createGain(); /* (3) Create input (typically recorder) */ this.input = null; /* (4) Create network I/O controllers (WebSocket) */ this.network = { out: this.ctx.createScriptProcessor(AudioManager.BUFFER_SIZE, 1, 1), in: null // will contain NETWORK:IN source node }; /* (5) Bind network routines */ this.network.out.onaudioprocess = this.send.bind(this); /* (5) Specify node chains */ this.node = { input: [ this.ctx.createAnalyser() ], // INPUT connects to it netout: [] // INPUT chains through it until NETOUT }; /* (6) Create output + bind gain */ this.output = this.ctx.destination; /* (7) Create websocket connection */ this.ws = new WebSocket('wss://ws.douscord.xdrm.io/audio/2'); let self = this; /* (8) Manage websocket requests */ this._ws = { stack: [], send(_data){ if( self.ws.readyState !== 1 ) // not connected -> stack return self._ws.stack.push(_data); console.warn(`send buffer[${_data.length}]`); self.ws.send(_data); }, }; /* (9) Manage websocket message stack */ this.ws.onopen = function(){ console.warn(`pop stack of size ${this._ws.stack.length}`); while( this._ws.stack.length > 0 ) this.ws.send(this._ws.stack.shift()); }.bind(this); /* (10) Manage websocket responses */ this.ws.onmessage = function(_msg){ console.warn(`received`, _msg.data); if( !(_msg.data instanceof Blob) ) return console.warn('NIQUE'); let fr = new FileReader(); fr.onload = function(){ let buf16 = new Int16Array(this.result); self.receive(bu16).bind(self); }; fr.readAsArrayBuffer(_msg.data); } } /* (1) Binds an input stream * ---------------------------------------------------------*/ bind(){ let current_node = null; /* (1) Bind INPUT ------> NETWORK:OUT circuit ---------------------------------------------------------*/ current_node = this.input; /* (1) Connect INPUT to input list */ for( let node of this.node.input ) current_node.connect(node); /* (2) Chain INPUT to input chain */ for( let node of this.node.netout ){ current_node.connect(node); current_node = node; } /* (3) Finally connect to NETWORK:OUT */ current_node.connect(this.network.out); /* (2) Bind NETWORK:IN ------> OUTPUT circuit ---------------------------------------------------------*/ // WILL BE DONE ON receive() /* (1) Finally connect to OUTPUT */ current_node.connect(this.gain); this.gain.connect(this.output); } /* (2) Binds an input stream * ---------------------------------------------------------*/ bindRecorderStream(_stream){ /* (1) Bind audio stream */ this.input = this.ctx.createMediaStreamSource(_stream); } /* (3) Sharing process implementation * ---------------------------------------------------------*/ send(_audioprocess){ /*DEBUG*///console.warn('time of', 16*2048/8, 'bytes in ', new Date().getTime()-window.timer); /*DEBUG*///window.timer = new Date().getTime(); let buf32 = new Float32Array(AudioManager.BUFFER_SIZE); _audioprocess.inputBuffer.copyFromChannel(buf32, 0); let buf16 = this.f32toi16(buf32); console.log(`read buffer[${buf16.length}]`); this._ws.send(buf16); } receive(_buffer){ console.log(`play buffer[${_buffer.length}]`); /* (1) Convert to Float32Array */ let buf32 = this.i16tof32(_buffer); /* (2) Create source node */ this.network.in = this.ctx.createBufferSource(); /* (3) Create buffer and dump data */ let input_buffer = this.ctx.createBuffer(1, AudioManager.BUFFER_SIZE, this.ctx.sampleRate); input_buffer.getChannelData(0).set(buf32); /* (4) Pass buffer to source node */ this.network.in.buffer = input_buffer; /* (5) Connect and play audio */ this.network.in.connect(this.gain); } /* (4) Convert Float32Array to Int16Array * * @buf32 Input * * @return buf16 Converted output * ---------------------------------------------------------*/ f32toi16(buf32){ /* (1) Initialise output */ let buf16 = new Int16Array(buf32.length); /* (2) Initialize loop */ let i = 0, l = buf32.length; /* (3) Convert each value */ for( ; i < l ; i++ ) buf16[i] = (buf32[i] < 0) ? 0x8000 * buf32[i] : 0x7FFF * buf32[i]; return buf16; } /* (2) Convert Int16Array to Float32Array * * @buf16 Input * * @return buf32 Converted output * ---------------------------------------------------------*/ i16tof32(buf16){ /* (1) Initialise output */ let buf32 = new Float32Array(buf16.length); /* (2) Initialize loop */ let i = 0, l = buf16.length; /* (3) Convert each value */ for( ; i < l ; i++ ) buf32[i] = (buf16[i] >= 0x8000) ? -(0x10000 * buf16[i])/0x8000 : buf16[i] / 0x7FFF; return buf32; } /* (x) Access microphone + launch all * ---------------------------------------------------------*/ launch(){ window.recorder = null; if( navigator.mediaDevices && navigator.mediaDevices.getUserMedia ){ navigator.mediaDevices.getUserMedia({ audio: true }) .then( stream => { recorder = new MediaRecorder(stream); this.bindRecorderStream(stream); this.bind(); recorder.onstart = () => console.warn('start'); recorder.onstop = () => { recorder.stream.getTracks().map( t => t.stop() ); }; // start recording recorder.start(); }) .catch( e => console.warn('error getting audio stream', e) ); }else console.warn('getUserMedia() not supported'); } }