À LA DÉCOUVERTE DES API Web Audio 🎧 ET Web MIDI 🎹
THAT'S THE QUESTION
THAT 80'S SHOW
BECAUSE SILENCE IS OVERRATED
GÉNÉRER DES SIGNAUX PÉRIODIQUES
GÉNÉRER DES SIGNAUX PÉRIODIQUES
UN EXEMPLE SIMPLE
DÉFINITION DE LA CLASSE
export class WebAudioExample {
constructor() {
/* code */
}
play(type) {
/* code */
}
stop() {
/* code */
}
set_freq(value) {
/* code */
}
set_gain(value) {
/* code */
}
}
LE CONSTRUCTEUR
export class WebAudioExample {
constructor() {
this.waContext = null;
this.waGain = null;
this.waOscillator = null;
}
play(type) {
/* code */
}
stop() {
/* code */
}
set_freq(value) {
/* code */
}
set_gain(value) {
/* code */
}
}
LA MÉTHODE play()
export class WebAudioExample {
constructor() {
/* code */
}
play(type) {
if(this.waContext == null) {
this.waContext = new AudioContext();
}
if(this.waGain == null) {
this.waGain = new GainNode(this.waContext);
this.waGain.connect(this.waContext.destination);
}
if(this.waOscillator == null) {
this.waOscillator = new OscillatorNode(this.waContext, {
type: type
});
this.waOscillator.connect(this.waGain);
this.waOscillator.start();
}
else {
this.waOscillator.type = type;
}
}
stop() {
/* code */
}
set_freq(value) {
/* code */
}
set_gain(value) {
/* code */
}
}
LA MÉTHODE stop()
export class WebAudioExample {
constructor() {
/* code */
}
play(type) {
/* code */
}
stop() {
if(this.waOscillator != null) {
this.waOscillator.stop();
this.waOscillator.disconnect();
this.waOscillator = null;
}
}
set_freq(value) {
/* code */
}
set_gain(value) {
/* code */
}
}
LA MÉTHODE set_freq()
export class WebAudioExample {
constructor() {
/* code */
}
play(type) {
/* code */
}
stop() {
/* code */
}
set_freq(value) {
if(this.waOscillator != null) {
this.waOscillator.frequency.setValueAtTime(value, this.waContext.currentTime);
}
}
set_gain(value) {
/* code */
}
}
LA MÉTHODE set_gain()
export class WebAudioExample {
constructor() {
/* code */
}
play(type) {
/* code */
}
stop() {
/* code */
}
set_freq(value) {
/* code */
}
set_gain(value) {
if(this.waGain != null) {
this.waGain.gain.value = (value / 100.0);
}
}
}
GÉNÉRER ET MANIPULER DES SAMPLES
GÉNÉRER ET MANIPULER DES SAMPLES
STRUCTURE GLOBALE
export class MyAudioWorkletProcessor extends AudioWorkletProcessor {
constructor(options) {
super();
/* some setup */
}
process(inputs, outputs, parameters) {
/* some processing */
return true;
}
}
registerProcessor("my-audio-worklet-processor", MyAudioWorkletProcessor);
UTILISATION
async function initialize() {
let context = new AudioContext();
await context.audioWorklet.addModule('my-audio-worklet-processor.js');
let worklet = new AudioWorkletNode(context, 'my-audio-worklet-processor', {
numberOfInputs: 0,
numberOfOutputs: 1,
outputChannelCount: [2],
});
worklet.connect(context.destination);
}
UN EXEMPLE SIMPLE
DÉFINITION DE LA CLASSE
export class WebAudioExample {
constructor() {
/* code */
}
async play(freq) {
/* code */
}
async stop() {
/* code */
}
async set_freq(value) {
/* code */
}
async set_gain(value) {
/* code */
}
}
LE CONSTRUCTEUR
export class WebAudioExample {
constructor() {
this.waContext = null;
this.waGain = null;
this.waWorklet = null;
}
async play(freq) {
/* code */
}
async stop() {
/* code */
}
async set_freq(value) {
/* code */
}
async set_gain(value) {
/* code */
}
}
LA MÉTHODE play()
export class WebAudioExample {
constructor() {
/* code */
}
async play(freq) {
if(this.waContext == null) {
this.waContext = new AudioContext();
await this.waContext.audioWorklet.addModule('audio-worklet-processor-example.js');
}
if(this.waGain == null) {
this.waGain = new GainNode(this.waContext);
this.waGain.connect(this.waContext.destination);
}
if(this.waWorklet == null) {
this.waWorklet = new AudioWorkletNode(this.waContext, 'audio-worklet-processor-example', {
numberOfInputs: 0,
numberOfOutputs: 1,
outputChannelCount: [2]
});
this.waWorklet.connect(this.waGain);
}
if(this.waWorklet != null) {
this.waWorklet.port.postMessage({ type: 'Play', data: freq });
}
}
async stop() {
/* code */
}
async set_freq(value) {
/* code */
}
async set_gain(value) {
/* code */
}
}
LA MÉTHODE stop()
export class WebAudioExample {
constructor() {
/* code */
}
async play(freq) {
/* code */
}
async stop() {
if(this.waWorklet != null) {
this.waWorklet.port.postMessage({ type: 'Stop', data: 0 });
}
}
async set_freq(value) {
/* code */
}
async set_gain(value) {
/* code */
}
}
LA MÉTHODE set_freq()
export class WebAudioExample {
constructor() {
/* code */
}
async play(freq) {
/* code */
}
async stop() {
/* code */
}
async set_freq(value) {
if(this.waWorklet != null) {
this.waWorklet.port.postMessage({ type: 'Freq', data: value });
}
}
async set_gain(value) {
/* code */
}
}
LA MÉTHODE set_gain()
export class WebAudioExample {
constructor() {
/* code */
}
async play(freq) {
/* code */
}
async stop() {
/* code */
}
async set_freq(value) {
/* code */
}
async set_gain(value) {
if(this.waGain != null) {
this.waGain.gain.value = (value / 100.0);
}
}
}
LE PROCESSEUR
export class AudioWorkletProcessorExample extends AudioWorkletProcessor {
constructor(options) {
/* code */
}
process(inputs, outputs, parameters) {
/* code */
}
}
registerProcessor("audio-worklet-processor-example", AudioWorkletProcessorExample);
LE CONSTRUCTEUR
export class AudioWorkletProcessorExample extends AudioWorkletProcessor {
constructor(options) {
super();
this.freq = 0;
this.count = 0;
this.value = 0;
this.port.onmessage = (message) => {
const event = message.data;
switch(event.type) {
case 'Play':
this.freq = event.data;
this.value = 1;
break;
case 'Stop':
this.freq = 0;
this.value = 0;
break;
case 'Freq':
this.freq = event.data;
break;
default:
break;
}
};
}
process(inputs, outputs, parameters) {
/* code */
}
}
registerProcessor("audio-worklet-processor-example", AudioWorkletProcessorExample);
LA MÉTHODE process()
export class AudioWorkletProcessorExample extends AudioWorkletProcessor {
constructor(options) {
/* code */
}
process(inputs, outputs, parameters) {
const sampleFreq = this.freq * 2;
const sampleCount = outputs[0][0].length;
for(let sampleIndex = 0; sampleIndex < sampleCount; ++sampleIndex) {
this.count += sampleFreq;
if(this.count >= sampleRate) {
this.count -= sampleRate;
this.value = -this.value;
}
for(const output of outputs) {
for(const channel of output) {
channel[sampleIndex] = this.value;
}
}
}
return true;
}
}
registerProcessor("audio-worklet-processor-example", AudioWorkletProcessorExample);
DÉCLARATION DU PROCESSEUR
export class AudioWorkletProcessorExample extends AudioWorkletProcessor {
constructor(options) {
/* code */
}
process(inputs, outputs, parameters) {
/* code */
}
}
registerProcessor("audio-worklet-processor-example", AudioWorkletProcessorExample);
UN ÉMULATEUR DE AY-3-8910 EN JAVASCRIPT
PROGRAMMABLE SOUND GENERATOR
TRÈS UTILISÉ DANS LES ANNÉES 80
LES ENTRAILLES DU CHIP
INTERNAL STATE
class AYM_State {
constructor() {
this.index = 0;
this.array = new Uint8Array(16);
}
reset() {
this.index &= 0;
const count = this.array.length;
for(let index = 0; index < count; ++index) {
this.array[index] &= 0;
}
}
}
TONE GENERATOR
class AYM_ToneGenerator {
constructor() {
this.counter = 0;
this.period = 0;
this.phase = 0;
}
reset() {
this.counter &= 0;
this.period &= 0;
this.phase &= 0;
}
clock() {
if(++this.counter >= this.period) {
this.counter &= 0;
this.phase ^= 1;
}
}
set_coarse_tune(value) {
const msb = ((value & 0xff) << 8);
const lsb = (this.period & (0xff << 0));
this.period = (msb | lsb);
}
set_fine_tune(value) {
const msb = (this.period & (0xff << 8));
const lsb = ((value & 0xff) << 0);
this.period = (msb | lsb);
}
}
NOISE GENERATOR
class AYM_NoiseGenerator {
constructor() {
this.counter = 0;
this.period = 0;
this.phase = 0;
}
reset() {
this.counter &= 0;
this.period &= 0;
this.phase &= 0;
}
clock() {
if(++this.counter >= this.period) {
this.counter &= 0;
const lfsr = this.phase;
const bit0 = (lfsr << 16);
const bit3 = (lfsr << 13);
const msb = (~(bit0 ^ bit3) & 0x10000);
const lsb = ((lfsr >> 1) & 0x0ffff);
this.phase = (msb | lsb);
}
}
set_coarse_tune(value) {
const msb = ((value & 0xff) << 9);
const lsb = (this.period & (0xff << 1));
this.period = (msb | lsb);
}
set_fine_tune(value) {
const msb = (this.period & (0xff << 9));
const lsb = ((value & 0xff) << 1);
this.period = (msb | lsb);
}
}
ENVELOPE GENERATOR
class AYM_EnvelopeGenerator {
constructor() {
this.counter = 0;
this.period = 0;
this.shape = 0;
this.phase = 0;
this.level = 0;
const ramp_up = () => {
this.level = ((this.level + 1) & 0x1f);
if(this.level == 0x1f) {
this.phase ^= 1;
}
};
const ramp_down = () => {
this.level = ((this.level - 1) & 0x1f);
if(this.level == 0x00) {
this.phase ^= 1;
}
};
const hold_up = () => {
this.level = 0x1f;
};
const hold_down = () => {
this.level = 0x00;
};
this.cycles = [
[ ramp_down, hold_down ],
[ ramp_down, hold_down ],
[ ramp_down, hold_down ],
[ ramp_down, hold_down ],
[ ramp_up , hold_down ],
[ ramp_up , hold_down ],
[ ramp_up , hold_down ],
[ ramp_up , hold_down ],
[ ramp_down, ramp_down ],
[ ramp_down, hold_down ],
[ ramp_down, ramp_up ],
[ ramp_down, hold_up ],
[ ramp_up , ramp_up ],
[ ramp_up , hold_up ],
[ ramp_up , ramp_down ],
[ ramp_up , hold_down ],
];
}
reset() {
this.counter &= 0;
this.period &= 0;
this.shape &= 0;
this.phase &= 0;
this.level &= 0;
}
clock() {
if(++this.counter >= this.period) {
this.counter &= 0;
this.cycles[this.shape][this.phase]();
}
}
set_coarse_tune(value) {
const msb = ((value & 0xff) << 8);
const lsb = (this.period & (0xff << 0));
this.period = (msb | lsb);
}
set_fine_tune(value) {
const msb = (this.period & (0xff << 8));
const lsb = ((value & 0xff) << 0);
this.period = (msb | lsb);
}
set_shape(value) {
this.shape = value;
this.phase = 0;
this.level = ((this.shape & 0x04) != 0 ? 0x00 : 0x1f);
}
get_level() {
return this.level;
}
}
TONE AN NOISE MIXER
class AYM_ToneAndNoiseMixer {
constructor() {
this.sound0 = 0;
this.noise0 = 0;
this.level0 = 0;
this.sound1 = 0;
this.noise1 = 0;
this.level1 = 0;
this.sound2 = 0;
this.noise2 = 0;
this.level2 = 0;
}
reset() {
this.sound0 &= 0;
this.noise0 &= 0;
this.level0 &= 0;
this.sound1 &= 0;
this.noise1 &= 0;
this.level1 &= 0;
this.sound2 &= 0;
this.noise2 &= 0;
this.level2 &= 0;
}
clock(envelope) {
const level = envelope.get_level();
if((this.level0 & 0x20) != 0) {
this.level0 = ((this.level0 & 0xe0) | (level & 0x1f));
}
if((this.level1 & 0x20) != 0) {
this.level1 = ((this.level1 & 0xe0) | (level & 0x1f));
}
if((this.level2 & 0x20) != 0) {
this.level2 = ((this.level2 & 0xe0) | (level & 0x1f));
}
}
set_configuration(value) {
this.sound0 = (((value & 0x01) == 0) | 0);
this.sound1 = (((value & 0x02) == 0) | 0);
this.sound2 = (((value & 0x04) == 0) | 0);
this.noise0 = (((value & 0x08) == 0) | 0);
this.noise1 = (((value & 0x10) == 0) | 0);
this.noise2 = (((value & 0x20) == 0) | 0);
}
set_channel0_amplitude(value) {
this.level0 = ((value << 1) | (value & 0x01));
}
set_channel1_amplitude(value) {
this.level1 = ((value << 1) | (value & 0x01));
}
set_channel2_amplitude(value) {
this.level2 = ((value << 1) | (value & 0x01));
}
}
CHIP EMULATOR
export class AYM_Emulator {
constructor(setup) {
this.state = new AYM_State();
this.tone0 = new AYM_ToneGenerator();
this.tone1 = new AYM_ToneGenerator();
this.tone2 = new AYM_ToneGenerator();
this.noise = new AYM_NoiseGenerator();
this.envel = new AYM_EnvelopeGenerator();
this.mixer = new AYM_ToneAndNoiseMixer();
this.master_clock = 1000000;
this.clock_divide = 0;
this.dac = AY_DAC;
this.set_type(setup.type || 'default');
this.reset();
}
set_type(type) {
switch(type) {
case 'AY':
this.set_type_ay();
break;
case 'YM':
this.set_type_ym();
break;
default:
break;
}
}
set_type_ay() {
this.dac = AY_DAC;
}
set_type_ym() {
this.dac = YM_DAC;
}
reset() {
this.state.reset();
this.tone0.reset();
this.tone1.reset();
this.tone2.reset();
this.noise.reset();
this.envel.reset();
this.mixer.reset();
this.master_clock |= 0;
this.clock_divide &= 0;
for(let index = 0; index < 16; ++index) {
this.set_register_index(index);
this.set_register_value(0);
this.set_register_index(0);
}
}
clock() {
const clk_div = (this.clock_divide = ((this.clock_divide + 1) & 0xff));
if((clk_div & 0x07) == 0) {
this.fixup_tones();
this.tone0.clock();
this.tone1.clock();
this.tone2.clock();
this.noise.clock();
this.envel.clock();
this.mixer.clock(this.envel);
}
}
fixup_tones() {
const fixup = (lhs, rhs) => {
if((lhs.period == rhs.period) && (lhs.counter != rhs.counter)) {
lhs.counter = rhs.counter;
lhs.phase = rhs.phase;
}
};
fixup(this.tone0, this.tone1);
fixup(this.tone0, this.tone2);
fixup(this.tone1, this.tone2);
}
set_register_index(reg_index) {
this.state.index = (reg_index &= 0xff);
return reg_index;
}
get_register_value(reg_value) {
const reg_index = this.state.index;
const reg_array = this.state.array;
if((reg_index >= 0) && (reg_index <= 15)) {
reg_value = reg_array[reg_index];
}
return reg_value;
}
set_register_value(reg_value) {
const reg_index = this.state.index;
const reg_array = this.state.array;
switch(reg_index) {
case 0x00: /* CHANNEL_A_FINE_TUNE */
reg_array[reg_index] = (reg_value &= 0xff);
this.tone0.set_fine_tune(reg_value);
break;
case 0x01: /* CHANNEL_A_COARSE_TUNE */
reg_array[reg_index] = (reg_value &= 0x0f);
this.tone0.set_coarse_tune(reg_value);
break;
case 0x02: /* CHANNEL_B_FINE_TUNE */
reg_array[reg_index] = (reg_value &= 0xff);
this.tone1.set_fine_tune(reg_value);
break;
case 0x03: /* CHANNEL_B_COARSE_TUNE */
reg_array[reg_index] = (reg_value &= 0x0f);
this.tone1.set_coarse_tune(reg_value);
break;
case 0x04: /* CHANNEL_C_FINE_TUNE */
reg_array[reg_index] = (reg_value &= 0xff);
this.tone2.set_fine_tune(reg_value);
break;
case 0x05: /* CHANNEL_C_COARSE_TUNE */
reg_array[reg_index] = (reg_value &= 0x0f);
this.tone2.set_coarse_tune(reg_value);
break;
case 0x06: /* NOISE_GENERATOR */
reg_array[reg_index] = (reg_value &= 0x1f);
this.noise.set_fine_tune(reg_value);
break;
case 0x07: /* MIXER_AND_IO_CONTROL */
reg_array[reg_index] = (reg_value &= 0xff);
this.mixer.set_configuration(reg_value);
break;
case 0x08: /* CHANNEL_A_AMPLITUDE */
reg_array[reg_index] = (reg_value &= 0x1f);
this.mixer.set_channel0_amplitude(reg_value);
break;
case 0x09: /* CHANNEL_B_AMPLITUDE */
reg_array[reg_index] = (reg_value &= 0x1f);
this.mixer.set_channel1_amplitude(reg_value);
break;
case 0x0a: /* CHANNEL_C_AMPLITUDE */
reg_array[reg_index] = (reg_value &= 0x1f);
this.mixer.set_channel2_amplitude(reg_value);
break;
case 0x0b: /* ENVELOPE_FINE_TUNE */
reg_array[reg_index] = (reg_value &= 0xff);
this.envel.set_fine_tune(reg_value);
break;
case 0x0c: /* ENVELOPE_COARSE_TUNE */
reg_array[reg_index] = (reg_value &= 0xff);
this.envel.set_coarse_tune(reg_value);
break;
case 0x0d: /* ENVELOPE_SHAPE */
reg_array[reg_index] = (reg_value &= 0x0f);
this.envel.set_shape(reg_value);
break;
case 0x0e: /* IO_PORT_A */
reg_array[reg_index] = (reg_value &= 0xff);
break;
case 0x0f: /* IO_PORT_B */
reg_array[reg_index] = (reg_value &= 0xff);
break;
default:
break;
}
return reg_value;
}
get_master_clock() {
return this.master_clock;
}
set_master_clock(master_clock) {
this.master_clock = (master_clock | 0);
if(this.master_clock <= 0) {
this.master_clock = 1000000;
}
return this.master_clock;
}
get_channel0() {
const sound = (this.tone0.phase & this.mixer.sound0);
const noise = (this.noise.phase & this.mixer.noise0);
const level = (this.dac[this.mixer.level0 & 0x1f]);
return ((sound | noise) * level);
}
get_channel1() {
const sound = (this.tone1.phase & this.mixer.sound1);
const noise = (this.noise.phase & this.mixer.noise1);
const level = (this.dac[this.mixer.level1 & 0x1f]);
return ((sound | noise) * level);
}
get_channel2() {
const sound = (this.tone2.phase & this.mixer.sound2);
const noise = (this.noise.phase & this.mixer.noise2);
const level = (this.dac[this.mixer.level2 & 0x1f]);
return ((sound | noise) * level);
}
}
AYM AUDIO WORKLET PROCESSOR
export class AYM_Processor extends AudioWorkletProcessor {
constructor(options) {
/* code */
}
process(inputs, outputs, parameters) {
/* code */
}
}
AYM AUDIO WORKLET PROCESSOR
export class AYM_Processor extends AudioWorkletProcessor {
constructor(options) {
super();
this.chip = new AYM_Emulator();
this.channel_a = new Float32Array(256);
this.channel_b = new Float32Array(256);
this.channel_c = new Float32Array(256);
this.count = 0;
this.clock = 1000000;
this.chip.set_master_clock(this.clock);
this.chip.reset();
this.port.onmessage = (message) => {
/* process messages here */
};
}
process(inputs, outputs, parameters) {
/* code */
}
}
AYM AUDIO WORKLET PROCESSOR
export class AYM_Processor extends AudioWorkletProcessor {
constructor(options) {
/* code */
}
process(inputs, outputs, parameters) {
const count = outputs[0][0].length;
for(let index = 0; index < count; ++index) {
this.channel_a[index] = this.chip.get_channel0();
this.channel_b[index] = this.chip.get_channel1();
this.channel_c[index] = this.chip.get_channel2();
while(this.count < this.clock) {
this.count += sampleRate;
this.chip.clock();
}
this.count -= this.clock;
}
for(const output of outputs) {
if(output.length >= 2) {
this.mixStereo(output[0], output[1]);
continue;
}
if(output.length >= 1) {
this.mixMono(output[0]);
continue;
}
}
return true;
}
}
UN LECTEUR DE MUSIQUE
YOUR BROWSER · YOUR HOME STUDIO
UTILISATION
function midiInitialize() {
/* code */
}
function midiSuccess(midi) {
/* code */
}
function midiFailure(midi) {
/* code */
}
UTILISATION
function midiInitialize() {
navigator.requestMIDIAccess().then(
(midi) => { midiSuccess(midi) },
(midi) => { midiFailure(midi) }
);
}
function midiSuccess(midi) {
/* code */
}
function midiFailure(midi) {
/* code */
}
UTILISATION
function midiInitialize() {
/* code */
}
function midiSuccess(midi) {
midi.inputs.forEach((input) => {
input.onmidimessage = (message) => {
switch((message.data[0] >> 4)) {
case 0x8: midiNoteOff(message);
break;
case 0x9: midiNoteOn(message);
break;
case 0xa: midiAftertouch(message);
break;
case 0xb: midiControlChange(message);
break;
case 0xc: midiProgramChange(message);
break;
case 0xd: midiChannelPressure(message);
break;
case 0xe: midiPitchBend(message);
break;
case 0xf: midiSystemControl(message);
break;
default:
break;
}
};
});
}
function midiFailure(midi) {
/* code */
}
UTILISATION
function midiInitialize() {
/* code */
}
function midiSuccess(midi) {
/* code */
}
function midiFailure(midi) {
throw new Error('Error while requesting MIDI access!');
}
UN SYNTHÉTISEUR MIDI POLYPHONIQUE
THE SOUND OF SILENCE
@ponceto91 | emaxilde.net |
@ponceto91 | github.com/ponceto/ |
@ponceto91 | gitlab.com/ponceto/ |
@ponceto91 | bitbucket.org/ponceto/ |