2018-04-05 22:54:13 +00:00
|
|
|
export default class AudioManager{
|
|
|
|
|
2018-04-06 16:39:14 +00:00
|
|
|
static get BUFFER_SIZE(){ return 4096; }
|
2018-04-05 22:54:13 +00:00
|
|
|
|
|
|
|
|
|
|
|
constructor(){
|
|
|
|
|
2018-04-06 10:43:49 +00:00
|
|
|
/* (1) Initialise our AudioNodes
|
|
|
|
---------------------------------------------------------*/
|
2018-04-05 22:54:13 +00:00
|
|
|
/* (1) Build Audio Context */
|
2018-04-06 10:43:49 +00:00
|
|
|
this.ctx = new (window.AudioContext || window.webkitAudioContext)();
|
|
|
|
|
|
|
|
/* (2) Create the MASTER gain */
|
|
|
|
this.master = this.ctx.createGain();
|
2018-04-05 22:54:13 +00:00
|
|
|
|
2018-04-06 10:43:49 +00:00
|
|
|
/* (3) Initialise input (typically bound from recorder) */
|
2018-04-05 22:54:13 +00:00
|
|
|
this.input = null;
|
|
|
|
|
2018-04-06 16:39:14 +00:00
|
|
|
/* (4) Initialize analyser (from input) + callback */
|
|
|
|
this.analyser = this.ctx.createAnalyser();
|
|
|
|
this.freq_drawer = null;
|
|
|
|
this.wave_drawer = null;
|
|
|
|
|
|
|
|
/* (5) Shortcut our output */
|
2018-04-06 10:43:49 +00:00
|
|
|
this.output = this.ctx.destination;
|
|
|
|
|
2018-04-06 16:39:14 +00:00
|
|
|
/* (6) Connect MASTER gain to output */
|
2018-04-06 10:43:49 +00:00
|
|
|
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()
|
|
|
|
};
|
|
|
|
|
2018-04-06 10:57:22 +00:00
|
|
|
/* (3) Create network I/O controller (WebSocket) */
|
2018-04-05 22:54:13 +00:00
|
|
|
this.network = {
|
2018-04-06 10:43:49 +00:00
|
|
|
out: this.ctx.createScriptProcessor(AudioManager.BUFFER_SIZE, 1, 1)
|
2018-04-05 22:54:13 +00:00
|
|
|
};
|
|
|
|
|
2018-04-06 10:57:22 +00:00
|
|
|
/* (4) Initialise websocket */
|
|
|
|
this.ws = null;
|
|
|
|
|
2018-04-06 10:43:49 +00:00
|
|
|
/* (5) Bind network controller to send() function */
|
2018-04-05 22:54:13 +00:00
|
|
|
this.network.out.onaudioprocess = this.send.bind(this);
|
|
|
|
|
2018-04-06 10:57:22 +00:00
|
|
|
/* (6) Set up our filters' parameters */
|
|
|
|
this.setUpFilters();
|
2018-04-06 10:43:49 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* (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
|
|
|
|
}
|
2018-04-05 22:54:13 +00:00
|
|
|
};
|
|
|
|
|
2018-04-06 14:15:15 +00:00
|
|
|
this.debug = () => setInterval(function(){
|
2018-04-05 22:54:13 +00:00
|
|
|
|
2018-04-06 10:43:49 +00:00
|
|
|
console.group('debug');
|
|
|
|
|
|
|
|
for( let k in this.data ){
|
|
|
|
console.log(`${this.data[k]} ${k}`)
|
|
|
|
this.data[k] = this.def[k]
|
|
|
|
}
|
2018-04-05 22:54:13 +00:00
|
|
|
|
2018-04-06 10:43:49 +00:00
|
|
|
console.groupEnd('debug');
|
|
|
|
|
|
|
|
}.bind(this.dbg), this.dbg.interval*1000);
|
2018-04-05 22:54:13 +00:00
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2018-04-06 10:43:49 +00:00
|
|
|
/* (2) Setup filters
|
2018-04-05 22:54:13 +00:00
|
|
|
*
|
|
|
|
---------------------------------------------------------*/
|
2018-04-06 10:43:49 +00:00
|
|
|
setUpFilters(){
|
2018-04-05 22:54:13 +00:00
|
|
|
|
2018-04-06 10:43:49 +00:00
|
|
|
/* (1) Setup filter parameters
|
|
|
|
---------------------------------------------------------*/
|
|
|
|
/* (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;
|
|
|
|
|
|
|
|
/* (2) Setup EQ#2 -> voice fullness */
|
|
|
|
this.filters.voice_fullness.type = 'peaking';
|
|
|
|
this.filters.voice_fullness.frequency.value = 200;
|
|
|
|
this.filters.voice_fullness.Q.value = .8;
|
|
|
|
this.filters.voice_fullness.gain.value = 2;
|
|
|
|
|
|
|
|
/* (3) Setup EQ#3 -> reduce voice presence */
|
|
|
|
this.filters.voice_presence.type = 'peaking';
|
|
|
|
this.filters.voice_presence.frequency.value = 5000;
|
|
|
|
this.filters.voice_presence.Q.value = .8;
|
|
|
|
this.filters.voice_presence.gain.value = -2;
|
|
|
|
|
|
|
|
/* (4) Setup EQ#3 -> reduce 'sss' metallic sound */
|
|
|
|
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) Connect filters
|
|
|
|
---------------------------------------------------------*/
|
|
|
|
/* (1) Connect clarity to fullness */
|
|
|
|
this.filters.voice_clarity.connect( this.filters.voice_fullness );
|
2018-04-05 22:54:13 +00:00
|
|
|
|
2018-04-06 10:43:49 +00:00
|
|
|
/* (2) Connect fullness to presence reduction */
|
|
|
|
this.filters.voice_fullness.connect( this.filters.voice_presence );
|
2018-04-05 22:54:13 +00:00
|
|
|
|
2018-04-06 10:43:49 +00:00
|
|
|
/* (3) Connect presence reduction to 'ss' removal */
|
|
|
|
this.filters.voice_presence.connect( this.filters.voice_sss );
|
2018-04-05 22:54:13 +00:00
|
|
|
|
|
|
|
|
2018-04-06 10:43:49 +00:00
|
|
|
}
|
2018-04-05 22:54:13 +00:00
|
|
|
|
2018-04-06 10:43:49 +00:00
|
|
|
/* (3) Filter toggle
|
|
|
|
*
|
|
|
|
* @unlink<boolean> Whether to unlink filters (directly bind to output)
|
|
|
|
*
|
|
|
|
---------------------------------------------------------*/
|
|
|
|
linkFilters(unlink=false){
|
2018-04-05 22:54:13 +00:00
|
|
|
|
2018-04-06 10:43:49 +00:00
|
|
|
/* (1) Disconnect all by default */
|
|
|
|
this.input.disconnect();
|
|
|
|
|
|
|
|
/* (2) Get first filter */
|
|
|
|
let first_filter = this.filters.voice_clarity;
|
2018-04-06 11:11:52 +00:00
|
|
|
let last_filter = this.filters.voice_sss;
|
2018-04-06 10:43:49 +00:00
|
|
|
|
2018-04-06 10:57:22 +00:00
|
|
|
/* (3) If unlink -> connect directly to NETWORK output */
|
2018-04-06 10:43:49 +00:00
|
|
|
if( unlink === true )
|
2018-04-06 10:57:22 +00:00
|
|
|
return this.input.connect(this.network.out);
|
2018-04-06 10:43:49 +00:00
|
|
|
|
|
|
|
/* (4) If linking -> connect input to filter stack */
|
|
|
|
this.input.connect(first_filter);
|
2018-04-05 22:54:13 +00:00
|
|
|
|
2018-04-06 11:11:52 +00:00
|
|
|
/* (5) If linking -> connect stack end to network.out */
|
|
|
|
last_filter.connect(this.network.out);
|
|
|
|
|
2018-04-05 22:54:13 +00:00
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2018-04-06 12:26:15 +00:00
|
|
|
/* (3) Binds an input stream
|
2018-04-05 22:54:13 +00:00
|
|
|
*
|
|
|
|
---------------------------------------------------------*/
|
|
|
|
bindRecorderStream(_stream){
|
|
|
|
|
2018-04-06 10:43:49 +00:00
|
|
|
/* (1) Bind audio stream
|
|
|
|
---------------------------------------------------------*/
|
2018-04-06 16:39:14 +00:00
|
|
|
/* (1) bind our audio stream to our source */
|
2018-04-05 22:54:13 +00:00
|
|
|
this.input = this.ctx.createMediaStreamSource(_stream);
|
|
|
|
|
2018-04-06 16:39:14 +00:00
|
|
|
|
2018-04-06 10:43:49 +00:00
|
|
|
/* (2) By default: link through filters to output
|
|
|
|
---------------------------------------------------------*/
|
2018-04-06 16:39:14 +00:00
|
|
|
/* (1) Link through filters */
|
2018-04-06 10:43:49 +00:00
|
|
|
this.linkFilters();
|
|
|
|
|
2018-04-06 16:39:14 +00:00
|
|
|
/* (2) Also link to analyser */
|
|
|
|
this.input.connect(this.analyser);
|
|
|
|
|
|
|
|
|
2018-04-06 14:44:15 +00:00
|
|
|
gs.get.audio_conn = 2; // voice connected
|
2018-04-06 10:43:49 +00:00
|
|
|
|
2018-04-05 22:54:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2018-04-06 12:26:15 +00:00
|
|
|
/* (4) Send chunks (Float32Array)
|
2018-04-05 22:54:13 +00:00
|
|
|
*
|
|
|
|
---------------------------------------------------------*/
|
|
|
|
send(_audioprocess){
|
2018-04-06 10:43:49 +00:00
|
|
|
|
2018-04-06 16:39:14 +00:00
|
|
|
/* (1) Manage analyser
|
|
|
|
---------------------------------------------------------*/
|
|
|
|
/* (1) Process only if 'freq_drawer' is set */
|
|
|
|
if( this.freq_drawer instanceof Function ){
|
2018-04-05 22:54:13 +00:00
|
|
|
|
2018-04-06 16:39:14 +00:00
|
|
|
// 1. Prepare array
|
|
|
|
let freqArray = new Uint8Array(this.analyser.frequencyBinCount);
|
|
|
|
|
|
|
|
// 2. Get frequency array
|
|
|
|
this.analyser.getByteFrequencyData(freqArray);
|
|
|
|
|
|
|
|
// 3. Send to callback
|
|
|
|
this.freq_drawer(freqArray);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
/* (2) Process only if 'wave_drawer' is set */
|
|
|
|
else if( this.wave_drawer instanceof Function ){
|
|
|
|
|
|
|
|
// 1. Prepare array
|
|
|
|
let waveArray = new Uint8Array(this.analyser.fftSize);
|
|
|
|
|
|
|
|
// 2. Get wave array
|
|
|
|
this.analyser.getByteTimeDomainData(waveArray);
|
2018-04-05 22:54:13 +00:00
|
|
|
|
2018-04-06 16:39:14 +00:00
|
|
|
// 3. Send to callback
|
|
|
|
this.wave_drawer(waveArray);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* (2) WebSocket send packet
|
|
|
|
---------------------------------------------------------*/
|
|
|
|
/* (1) Exit here if not connected */
|
2018-04-06 10:57:22 +00:00
|
|
|
if( this.ws === null || this.ws.readyState !== 1 )
|
|
|
|
return;
|
|
|
|
|
2018-04-06 16:39:14 +00:00
|
|
|
/* (2) Initialize buffer (Float32Array) */
|
|
|
|
let buf32 = new Float32Array(AudioManager.BUFFER_SIZE);
|
|
|
|
|
|
|
|
/* (3) Extract stream into buffer */
|
|
|
|
_audioprocess.inputBuffer.copyFromChannel(buf32, 0);
|
|
|
|
|
|
|
|
/* (4) Convert for WS connection (Int16Array) */
|
|
|
|
let buf16 = this.f32toi16(buf32);
|
|
|
|
|
|
|
|
/* (5) Send buffer through websocket */
|
2018-04-06 00:21:27 +00:00
|
|
|
this.ws.send(buf16);
|
2018-04-05 22:54:13 +00:00
|
|
|
|
2018-04-06 16:39:14 +00:00
|
|
|
|
|
|
|
// DEBUG
|
2018-04-06 10:43:49 +00:00
|
|
|
this.dbg.data.packets_sent++;
|
|
|
|
this.dbg.data.kB_sent += buf16.length * 16. / 8 / 1024;
|
2018-04-05 22:54:13 +00:00
|
|
|
}
|
|
|
|
|
2018-04-06 12:26:15 +00:00
|
|
|
/* (5) Play received chunks (Int16Array)
|
2018-04-06 10:43:49 +00:00
|
|
|
*
|
|
|
|
---------------------------------------------------------*/
|
2018-04-05 22:54:13 +00:00
|
|
|
receive(_buffer){
|
|
|
|
|
|
|
|
/* (1) Convert to Float32Array */
|
|
|
|
let buf32 = this.i16tof32(_buffer);
|
|
|
|
|
|
|
|
/* (2) Create source node */
|
2018-04-06 10:43:49 +00:00
|
|
|
let source = this.ctx.createBufferSource();
|
2018-04-05 22:54:13 +00:00
|
|
|
|
|
|
|
/* (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);
|
|
|
|
|
2018-04-06 10:43:49 +00:00
|
|
|
/* (4) Bind buffer to source node */
|
|
|
|
source.buffer = input_buffer;
|
|
|
|
|
|
|
|
/* (5) Create a dedicated *muted* gain */
|
|
|
|
let gain = this.ctx.createGain();
|
|
|
|
|
|
|
|
/* (6) source -> gain -> MASTER + play() */
|
|
|
|
source.connect(gain);
|
|
|
|
gain.connect(this.master);
|
|
|
|
|
|
|
|
/* (7) Start playing */
|
|
|
|
source.start(this.ctx.currentTime);
|
2018-04-05 22:54:13 +00:00
|
|
|
|
2018-04-06 10:43:49 +00:00
|
|
|
this.dbg.data.packets_received++;
|
|
|
|
this.dbg.data.kB_received += _buffer.length * 16. / 8 / 1024;
|
2018-04-05 22:54:13 +00:00
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2018-04-06 12:26:15 +00:00
|
|
|
/* (6) Convert Float32Array to Int16Array
|
2018-04-05 22:54:13 +00:00
|
|
|
*
|
|
|
|
* @buf32<Float32Array> Input
|
|
|
|
*
|
|
|
|
* @return buf16<Int16Array> 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;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2018-04-06 12:26:15 +00:00
|
|
|
/* (7) Convert Int16Array to Float32Array
|
2018-04-05 22:54:13 +00:00
|
|
|
*
|
|
|
|
* @buf16<Int16Array> Input
|
|
|
|
*
|
|
|
|
* @return buf32<Float32Array> 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;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2018-04-05 23:42:56 +00:00
|
|
|
|
|
|
|
/* (8) Connect websocket
|
|
|
|
*
|
|
|
|
* @address<String> Websocket address
|
|
|
|
*
|
|
|
|
---------------------------------------------------------*/
|
|
|
|
wsconnect(_addr){
|
|
|
|
|
|
|
|
/* (1) Create websocket connection */
|
|
|
|
this.ws = new WebSocket(_addr);
|
2018-04-06 14:44:15 +00:00
|
|
|
gs.get.audio_conn = 0; // connecting
|
2018-04-05 23:42:56 +00:00
|
|
|
|
|
|
|
/* (2) Manage websocket responses */
|
|
|
|
this.ws.onmessage = function(_msg){
|
|
|
|
|
|
|
|
if( !(_msg.data instanceof Blob) )
|
|
|
|
return console.warn('[NaB] Not A Blob');
|
|
|
|
|
|
|
|
let fr = new FileReader();
|
|
|
|
|
|
|
|
fr.onload = function(){
|
|
|
|
|
|
|
|
let buf16 = new Int16Array(fr.result);
|
|
|
|
this.receive(buf16);
|
|
|
|
|
|
|
|
}.bind(this);
|
|
|
|
|
|
|
|
fr.readAsArrayBuffer(_msg.data);
|
|
|
|
|
|
|
|
}.bind(this);
|
|
|
|
|
2018-04-06 10:43:49 +00:00
|
|
|
/* (3) Debug */
|
2018-04-06 14:44:15 +00:00
|
|
|
this.ws.onopen = () => ( gs.get.audio_conn = 1 ); // listening
|
|
|
|
this.ws.onclose = () => ( gs.get.audio_conn = null ); // disconnected
|
2018-04-06 10:43:49 +00:00
|
|
|
|
|
|
|
|
2018-04-05 23:42:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
2018-04-06 12:26:15 +00:00
|
|
|
/* (9) Access microphone + launch all
|
2018-04-05 22:54:13 +00:00
|
|
|
*
|
|
|
|
---------------------------------------------------------*/
|
2018-04-05 23:42:56 +00:00
|
|
|
launch(wsAddress='wss://ws.douscord.xdrm.io/audio/2'){
|
|
|
|
|
|
|
|
/* (1) Start websocket */
|
|
|
|
this.wsconnect(wsAddress);
|
2018-04-05 22:54:13 +00:00
|
|
|
|
|
|
|
if( navigator.mediaDevices && navigator.mediaDevices.getUserMedia ){
|
|
|
|
|
|
|
|
navigator.mediaDevices.getUserMedia({ audio: true })
|
|
|
|
.then( stream => {
|
|
|
|
|
2018-04-06 10:43:49 +00:00
|
|
|
this.recorder = new MediaRecorder(stream);
|
2018-04-05 22:54:13 +00:00
|
|
|
this.bindRecorderStream(stream);
|
|
|
|
|
2018-04-06 10:43:49 +00:00
|
|
|
this.recorder.onstart = () => console.warn('[audio] recording');
|
|
|
|
this.recorder.onstop = () => {
|
|
|
|
this.recorder.stream.getTracks().map( t => t.stop() );
|
|
|
|
this.recorder = null;
|
|
|
|
console.warn('[audio] stopped recording');
|
2018-04-05 22:54:13 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
// start recording
|
2018-04-06 10:43:49 +00:00
|
|
|
this.recorder.start();
|
2018-04-05 22:54:13 +00:00
|
|
|
|
|
|
|
})
|
2018-04-06 10:43:49 +00:00
|
|
|
.catch( e => console.warn('[audio] microphone permission issue', e) );
|
2018-04-05 22:54:13 +00:00
|
|
|
|
|
|
|
}else
|
2018-04-06 10:43:49 +00:00
|
|
|
console.warn('[audio] microphone not supported');
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2018-04-06 12:26:15 +00:00
|
|
|
/* (10) Shut down microphone + kill all
|
2018-04-06 10:43:49 +00:00
|
|
|
*
|
|
|
|
---------------------------------------------------------*/
|
|
|
|
kill(){
|
|
|
|
|
|
|
|
/* (1) Close websocket */
|
|
|
|
this.ws.close();
|
|
|
|
|
|
|
|
/* (2) Stop recording */
|
|
|
|
this.recorder.stop();
|
|
|
|
|
2018-04-05 22:54:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2018-04-06 10:43:49 +00:00
|
|
|
|
2018-04-06 12:26:15 +00:00
|
|
|
/* (11) Play a POP notification
|
|
|
|
*
|
|
|
|
---------------------------------------------------------*/
|
|
|
|
pop(){
|
|
|
|
|
|
|
|
|
|
|
|
/* (1) Base data */
|
|
|
|
let base_freq = 150;
|
|
|
|
let mods = [0, 75, 75]; // freq modulations (from base_freq)
|
|
|
|
let time_range = 0.05; // time between each modulation
|
|
|
|
let start = this.ctx.currentTime + 0.1;
|
|
|
|
|
|
|
|
/* (2) Build oscillator */
|
|
|
|
let osc = this.ctx.createOscillator();
|
|
|
|
osc.type = 'triangle';
|
|
|
|
|
|
|
|
/* (3) Create local gain to lower volume */
|
|
|
|
let local = this.ctx.createGain();
|
|
|
|
local.gain.setValueAtTime(0.3, 0);
|
|
|
|
|
|
|
|
/* (4) Connect all nodes to output */
|
|
|
|
osc.connect(local);
|
|
|
|
local.connect(this.master);
|
|
|
|
|
|
|
|
/* (5) Bind frequencies over time */
|
|
|
|
for( let i in mods )
|
|
|
|
osc.frequency.setValueAtTime(base_freq+mods[i], start + i*time_range );
|
|
|
|
|
|
|
|
/* (6) Start playing */
|
|
|
|
osc.start( start );
|
|
|
|
|
|
|
|
/* (7) Set when to stop playing */
|
|
|
|
osc.stop( start + time_range*mods.length );
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2018-04-05 22:54:13 +00:00
|
|
|
}
|