[room-controller] room.voice.nav() toggles AudioManager.launch() and AudioManager.kill()
[lib.audio-manager] added microphone voice filters + added debugger (every 10s) + added for each read source (from WS) a local gain [toworkon] + can listen without microphone permissions
This commit is contained in:
parent
10bd316093
commit
854adb244d
|
@ -5,124 +5,229 @@ export default class AudioManager{
|
||||||
|
|
||||||
constructor(){
|
constructor(){
|
||||||
|
|
||||||
|
/* (1) Initialise our AudioNodes
|
||||||
|
---------------------------------------------------------*/
|
||||||
/* (1) Build Audio Context */
|
/* (1) Build Audio Context */
|
||||||
this.ctx = new (window.AudioContext || window.webkitAudioContext)();
|
this.ctx = new (window.AudioContext || window.webkitAudioContext)();
|
||||||
this.gain = this.ctx.createGain();
|
|
||||||
|
|
||||||
/* (3) Create input (typically recorder) */
|
/* (2) Create the MASTER gain */
|
||||||
|
this.master = this.ctx.createGain();
|
||||||
|
|
||||||
|
/* (3) Initialise input (typically bound from recorder) */
|
||||||
this.input = null;
|
this.input = null;
|
||||||
|
|
||||||
/* (4) Create network I/O controllers (WebSocket) */
|
/* (4) Shortcut our output */
|
||||||
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;
|
this.output = this.ctx.destination;
|
||||||
|
|
||||||
/* (7) Initialise websocket */
|
/* (5) Connect MASTER gain to output */
|
||||||
|
this.master.connect(this.output);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* (2) Initialise processing attributes
|
||||||
|
---------------------------------------------------------*/
|
||||||
|
/* (1) Container for our recorder */
|
||||||
|
this.recorder = null;
|
||||||
|
|
||||||
|
/* (2) Initialise filters */
|
||||||
|
this.filters = {
|
||||||
|
voice_clarity: this.ctx.createBiquadFilter(),
|
||||||
|
voice_fullness: this.ctx.createBiquadFilter(),
|
||||||
|
voice_presence: this.ctx.createBiquadFilter(),
|
||||||
|
voice_sss: this.ctx.createBiquadFilter()
|
||||||
|
};
|
||||||
|
|
||||||
|
/* (3) Set up our filters' parameters */
|
||||||
|
this.setUpFilters();
|
||||||
|
|
||||||
|
/* (4) Create network I/O controller (WebSocket) */
|
||||||
|
this.network = {
|
||||||
|
out: this.ctx.createScriptProcessor(AudioManager.BUFFER_SIZE, 1, 1)
|
||||||
|
};
|
||||||
|
|
||||||
|
/* (5) Bind network controller to send() function */
|
||||||
|
this.network.out.onaudioprocess = this.send.bind(this);
|
||||||
|
|
||||||
|
/* (6) Initialise websocket */
|
||||||
this.ws = null;
|
this.ws = null;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* (9) Debug data */
|
||||||
|
this.dbg = {
|
||||||
|
interval: 10, // debug every ... second
|
||||||
|
|
||||||
|
def: {
|
||||||
|
packets_received: 0,
|
||||||
|
packets_sent: 0,
|
||||||
|
kB_received: 0,
|
||||||
|
kB_sent: 0
|
||||||
|
},
|
||||||
|
|
||||||
|
data: {
|
||||||
|
packets_received: 0,
|
||||||
|
packets_sent: 0,
|
||||||
|
kB_received: 0,
|
||||||
|
kB_sent: 0
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
setInterval(function(){
|
||||||
|
|
||||||
|
console.group('debug');
|
||||||
|
|
||||||
|
for( let k in this.data ){
|
||||||
|
console.log(`${this.data[k]} ${k}`)
|
||||||
|
this.data[k] = this.def[k]
|
||||||
|
}
|
||||||
|
|
||||||
|
console.groupEnd('debug');
|
||||||
|
|
||||||
|
}.bind(this.dbg), this.dbg.interval*1000);
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* (1) Binds an input stream
|
/* (2) Setup filters
|
||||||
*
|
*
|
||||||
---------------------------------------------------------*/
|
---------------------------------------------------------*/
|
||||||
bind(){
|
setUpFilters(){
|
||||||
|
|
||||||
let current_node = null;
|
/* (1) Setup filter parameters
|
||||||
|
|
||||||
|
|
||||||
/* (1) Bind INPUT ------> NETWORK:OUT circuit
|
|
||||||
---------------------------------------------------------*/
|
---------------------------------------------------------*/
|
||||||
current_node = this.input;
|
/* (1) Setup EQ#1 -> voice clarity */
|
||||||
|
this.filters.voice_clarity.type = 'peaking';
|
||||||
|
this.filters.voice_clarity.frequency.value = 3000;
|
||||||
|
this.filters.voice_clarity.Q.value = .8;
|
||||||
|
this.filters.voice_clarity.gain.value = 2;
|
||||||
|
|
||||||
/* (1) Connect INPUT to input list */
|
/* (2) Setup EQ#2 -> voice fullness */
|
||||||
for( let node of this.node.input )
|
this.filters.voice_fullness.type = 'peaking';
|
||||||
current_node.connect(node);
|
this.filters.voice_fullness.frequency.value = 200;
|
||||||
|
this.filters.voice_fullness.Q.value = .8;
|
||||||
|
this.filters.voice_fullness.gain.value = 2;
|
||||||
|
|
||||||
/* (2) Chain INPUT to input chain */
|
/* (3) Setup EQ#3 -> reduce voice presence */
|
||||||
for( let node of this.node.netout ){
|
this.filters.voice_presence.type = 'peaking';
|
||||||
current_node.connect(node);
|
this.filters.voice_presence.frequency.value = 5000;
|
||||||
current_node = node;
|
this.filters.voice_presence.Q.value = .8;
|
||||||
}
|
this.filters.voice_presence.gain.value = -2;
|
||||||
|
|
||||||
/* (3) Finally connect to NETWORK:OUT */
|
/* (4) Setup EQ#3 -> reduce 'sss' metallic sound */
|
||||||
current_node.connect(this.network.out);
|
this.filters.voice_sss.type = 'peaking';
|
||||||
|
this.filters.voice_sss.frequency.value = 7000;
|
||||||
|
this.filters.voice_sss.Q.value = .8;
|
||||||
|
this.filters.voice_sss.gain.value = -8;
|
||||||
|
|
||||||
|
|
||||||
/* (2) Bind NETWORK:IN ------> OUTPUT circuit
|
/* (2) Connect filters
|
||||||
---------------------------------------------------------*/
|
---------------------------------------------------------*/
|
||||||
// WILL BE DONE ON receive()
|
/* (1) Connect clarity to fullness */
|
||||||
|
this.filters.voice_clarity.connect( this.filters.voice_fullness );
|
||||||
|
|
||||||
|
/* (2) Connect fullness to presence reduction */
|
||||||
|
this.filters.voice_fullness.connect( this.filters.voice_presence );
|
||||||
|
|
||||||
|
/* (3) Connect presence reduction to 'ss' removal */
|
||||||
|
this.filters.voice_presence.connect( this.filters.voice_sss );
|
||||||
|
|
||||||
|
/* (4) Connect last filter to MASTER gain */
|
||||||
|
this.filters.voice_sss.connect(this.master);
|
||||||
|
|
||||||
/* (1) Finally connect to OUTPUT */
|
|
||||||
// current_node.connect(this.gain);
|
|
||||||
this.gain.connect(this.output);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* (3) Filter toggle
|
||||||
|
*
|
||||||
|
* @unlink<boolean> Whether to unlink filters (directly bind to output)
|
||||||
|
*
|
||||||
|
---------------------------------------------------------*/
|
||||||
|
linkFilters(unlink=false){
|
||||||
|
|
||||||
|
/* (1) Disconnect all by default */
|
||||||
|
this.input.disconnect();
|
||||||
|
|
||||||
|
/* (2) Get first filter */
|
||||||
|
let first_filter = this.filters.voice_clarity;
|
||||||
|
|
||||||
|
/* (3) If unlink -> connect directly to MASTER gain */
|
||||||
|
if( unlink === true )
|
||||||
|
return this.input.connect(this.master);
|
||||||
|
|
||||||
|
/* (4) If linking -> connect input to filter stack */
|
||||||
|
this.input.connect(first_filter);
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* (2) Binds an input stream
|
/* (2) Binds an input stream
|
||||||
*
|
*
|
||||||
---------------------------------------------------------*/
|
---------------------------------------------------------*/
|
||||||
bindRecorderStream(_stream){
|
bindRecorderStream(_stream){
|
||||||
|
|
||||||
/* (1) Bind audio stream */
|
/* (1) Bind audio stream
|
||||||
|
---------------------------------------------------------*/
|
||||||
this.input = this.ctx.createMediaStreamSource(_stream);
|
this.input = this.ctx.createMediaStreamSource(_stream);
|
||||||
|
|
||||||
|
/* (2) By default: link through filters to output
|
||||||
|
---------------------------------------------------------*/
|
||||||
|
this.linkFilters();
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* (3) Sharing process implementation
|
/* (3) Send chunks (Float32Array)
|
||||||
*
|
*
|
||||||
---------------------------------------------------------*/
|
---------------------------------------------------------*/
|
||||||
send(_audioprocess){
|
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);
|
let buf32 = new Float32Array(AudioManager.BUFFER_SIZE);
|
||||||
// _audioprocess.inputBuffer.copyFromChannel(buf32, 0);
|
|
||||||
_audioprocess.inputBuffer.copyFromChannel(buf32, 0);
|
_audioprocess.inputBuffer.copyFromChannel(buf32, 0);
|
||||||
|
|
||||||
let buf16 = this.f32toi16(buf32);
|
let buf16 = this.f32toi16(buf32);
|
||||||
|
|
||||||
this.ws.send(buf16);
|
this.ws.send(buf16);
|
||||||
|
|
||||||
|
this.dbg.data.packets_sent++;
|
||||||
|
this.dbg.data.kB_sent += buf16.length * 16. / 8 / 1024;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* (4) Play received chunks (Int16Array)
|
||||||
|
*
|
||||||
|
---------------------------------------------------------*/
|
||||||
receive(_buffer){
|
receive(_buffer){
|
||||||
|
|
||||||
/* (1) Convert to Float32Array */
|
/* (1) Convert to Float32Array */
|
||||||
let buf32 = this.i16tof32(_buffer);
|
let buf32 = this.i16tof32(_buffer);
|
||||||
|
|
||||||
/* (2) Create source node */
|
/* (2) Create source node */
|
||||||
this.network.in = new AudioBufferSourceNode(this.ctx, {
|
let source = this.ctx.createBufferSource();
|
||||||
playbackRate: 1.1
|
|
||||||
});
|
|
||||||
|
|
||||||
/* (3) Create buffer and dump data */
|
/* (3) Create buffer and dump data */
|
||||||
let input_buffer = this.ctx.createBuffer(1, AudioManager.BUFFER_SIZE, this.ctx.sampleRate);
|
let input_buffer = this.ctx.createBuffer(1, AudioManager.BUFFER_SIZE, this.ctx.sampleRate);
|
||||||
input_buffer.getChannelData(0).set(buf32);
|
input_buffer.getChannelData(0).set(buf32);
|
||||||
|
|
||||||
/* (4) Pass buffer to source node */
|
/* (4) Bind buffer to source node */
|
||||||
this.network.in.buffer = input_buffer;
|
source.buffer = input_buffer;
|
||||||
|
|
||||||
/* (5) Connect and play audio */
|
/* (5) Create a dedicated *muted* gain */
|
||||||
this.network.in.connect(this.gain);
|
let gain = this.ctx.createGain();
|
||||||
this.network.in.start(0);
|
|
||||||
|
/* (6) source -> gain -> MASTER + play() */
|
||||||
|
source.connect(gain);
|
||||||
|
gain.connect(this.master);
|
||||||
|
|
||||||
|
/* (7) Start playing */
|
||||||
|
source.start(this.ctx.currentTime);
|
||||||
|
|
||||||
|
|
||||||
|
this.dbg.data.packets_received++;
|
||||||
|
this.dbg.data.kB_received += _buffer.length * 16. / 8 / 1024;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -203,6 +308,11 @@ export default class AudioManager{
|
||||||
|
|
||||||
}.bind(this);
|
}.bind(this);
|
||||||
|
|
||||||
|
/* (3) Debug */
|
||||||
|
this.ws.onopen = () => console.warn('[audio] websocket connected');
|
||||||
|
this.ws.onclose = () => console.warn('[audio] websocket closed');
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -215,32 +325,49 @@ export default class AudioManager{
|
||||||
/* (1) Start websocket */
|
/* (1) Start websocket */
|
||||||
this.wsconnect(wsAddress);
|
this.wsconnect(wsAddress);
|
||||||
|
|
||||||
window.recorder = null;
|
|
||||||
|
|
||||||
if( navigator.mediaDevices && navigator.mediaDevices.getUserMedia ){
|
if( navigator.mediaDevices && navigator.mediaDevices.getUserMedia ){
|
||||||
|
|
||||||
navigator.mediaDevices.getUserMedia({ audio: true })
|
navigator.mediaDevices.getUserMedia({ audio: true })
|
||||||
.then( stream => {
|
.then( stream => {
|
||||||
|
|
||||||
recorder = new MediaRecorder(stream);
|
this.recorder = new MediaRecorder(stream);
|
||||||
this.bindRecorderStream(stream);
|
this.bindRecorderStream(stream);
|
||||||
this.bind();
|
|
||||||
|
|
||||||
recorder.onstart = () => console.warn('start');
|
this.recorder.onstart = () => console.warn('[audio] recording');
|
||||||
recorder.onstop = () => {
|
this.recorder.onstop = () => {
|
||||||
recorder.stream.getTracks().map( t => t.stop() );
|
this.recorder.stream.getTracks().map( t => t.stop() );
|
||||||
|
this.recorder = null;
|
||||||
|
console.warn('[audio] stopped recording');
|
||||||
};
|
};
|
||||||
|
|
||||||
// start recording
|
// start recording
|
||||||
recorder.start();
|
this.recorder.start();
|
||||||
|
|
||||||
})
|
})
|
||||||
.catch( e => console.warn('error getting audio stream', e) );
|
.catch( e => console.warn('[audio] microphone permission issue', e) );
|
||||||
|
|
||||||
}else
|
}else
|
||||||
console.warn('getUserMedia() not supported');
|
console.warn('[audio] microphone not supported');
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* (x) Shut down microphone + kill all
|
||||||
|
*
|
||||||
|
---------------------------------------------------------*/
|
||||||
|
kill(){
|
||||||
|
|
||||||
|
/* (1) Close websocket */
|
||||||
|
this.ws.close();
|
||||||
|
|
||||||
|
/* (2) Stop recording */
|
||||||
|
this.recorder.stop();
|
||||||
|
|
||||||
|
/* (3) Volume 0 */
|
||||||
|
this.master.gain.setValueAtTime(0, this.ctx.currentTime);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -50,6 +50,17 @@ export default class RoomController{
|
||||||
if( type === 'text' && window.csock instanceof wscd )
|
if( type === 'text' && window.csock instanceof wscd )
|
||||||
csock.send({ buffer: { rid: room.id } });
|
csock.send({ buffer: { rid: room.id } });
|
||||||
|
|
||||||
|
/* (6) If 'voice' room -> launch audio */
|
||||||
|
if( type === 'voice' ){
|
||||||
|
|
||||||
|
if( typeof this[type].current === 'number' )
|
||||||
|
AudioManager.launch();
|
||||||
|
else
|
||||||
|
AudioManager.kill();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* (6) Update buffer */
|
/* (6) Update buffer */
|
||||||
this._buffer[type] = {};
|
this._buffer[type] = {};
|
||||||
for( let r of this[type].list )
|
for( let r of this[type].list )
|
||||||
|
|
Loading…
Reference in New Issue