| name | webaudio-api |
| description | Web Audio API — AudioContext, oscillateurs, buffers, effets DSP, spatialisation, analyse temps réel, Tone.js, microphone, streaming audio dans le navigateur. |
| tags | ["audio","web","javascript","webaudio","tonejs","dsp","browser","audio-api","realtime"] |
| platforms | ["linux","macos","windows"] |
| related_skills | ["audio-processing","music-generation","midi-sequencing"] |
Web Audio API — Traitement Audio dans le Navigateur
Guide complet de l'API Web Audio : synthèse, effets, spatialisation, analyse temps réel, intégration Tone.js, microphone, streaming.
1. Architecture de la Web Audio API
1.1 Graphe audio
AudioContext (graphe orienté acyclique)
├── Source Nodes
│ ├── OscillatorNode → Synthèse d'onde
│ ├── AudioBufferSourceNode → Buffer audio
│ ├── MediaStreamSourceNode → Microphone / stream
│ └── MediaElementSourceNode → <audio> / <video>
│
├── Processing/Effect Nodes
│ ├── GainNode → Volume / mix
│ ├── BiquadFilterNode → Filtre IIR (passe-bas, etc.)
│ ├── ConvolverNode → Reverb convolution
│ ├── DelayNode → Ligne à retard
│ ├── WaveShaperNode → Distorsion
│ ├── DynamicsCompressorNode → Compression
│ ├── StereoPannerNode → Panning stéréo
│ └── PannerNode → Spatialisation 3D
│
└── Destination
└── AudioDestinationNode → Haut-parleurs
1.2 Création de l'AudioContext
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
console.log(`Sample rate: ${audioCtx.sampleRate}Hz`);
if (audioCtx.state === 'suspended') {
await audioCtx.resume();
}
const audioCtx2 = new AudioContext({
sampleRate: 48000,
latencyHint: 'interactive'
});
2. Synthèse Sonore de Base
2.1 Oscillateurs
const osc = audioCtx.createOscillator();
osc.type = 'sine';
osc.frequency.value = 440;
osc.detune.value = 0;
const gain = audioCtx.createGain();
gain.gain.value = 0.5;
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start();
osc.stop(audioCtx.currentTime + 2);
2.2 Synthèse par modulation
const carrier = audioCtx.createOscillator();
carrier.frequency.value = 440;
const modulator = audioCtx.createOscillator();
modulator.frequency.value = 5;
modulator.type = 'sine';
const modGain = audioCtx.createGain();
modGain.gain.value = 0.5;
modulator.connect(modGain);
modGain.connect(carrier.frequency);
const outputGain = audioCtx.createGain();
outputGain.gain.value = 0.3;
carrier.connect(outputGain);
outputGain.connect(audioCtx.destination);
carrier.start();
modulator.start();
const fmCarrier = audioCtx.createOscillator();
fmCarrier.frequency.value = 220;
const fmModulator = audioCtx.createOscillator();
fmModulator.frequency.value = ;
fmModGain = audioCtx.();
fmModGain.. = ;
fmModulator.(fmModGain);
fmModGain.(fmCarrier.);
fmCarrier.(audioCtx.);
fmCarrier.();
fmModulator.();
2.3 Enveloppe ADSR
class ADSREnvelope {
constructor(audioCtx, output) {
this.ctx = audioCtx;
this.output = output;
}
triggerAttack() {
const now = this.ctx.currentTime;
this.output.gain.cancelScheduledValues(now);
this.output.gain.setValueAtTime(0, now);
this.output.gain.linearRampToValueAtTime(1, now + 0.01);
this.output.gain.linearRampToValueAtTime(0.7, now + 0.1);
}
triggerRelease() {
const now = this.ctx.currentTime;
this.output.gain.cancelScheduledValues(now);
this.output.gain.(..., now);
...(, now + );
}
}
envGain = audioCtx.();
envGain.. = ;
env = (audioCtx, envGain);
osc.(envGain);
envGain.(audioCtx.);
env.();
( env.(), );
3. Effets Audio
3.1 Filtres (BiquadFilterNode)
const filter = audioCtx.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.value = 1000;
filter.Q.value = 1;
filter.gain.value = 0;
source.connect(filter);
filter.connect(audioCtx.destination);
filter.frequency.setValueAtTime(200, audioCtx.currentTime);
filter.frequency.exponentialRampToValueAtTime(8000, audioCtx.currentTime + 3);
const delay = audioCtx.createDelay(0.1);
delay.delayTime.value = 0.005;
const feedback = audioCtx.createGain();
feedback.gain.value = 0.3;
source.connect(delay);
delay.connect(feedback);
feedback.(delay);
delay.(audioCtx.);
3.2 Reverb (ConvolverNode)
async function createReverb(audioCtx, irUrl, mix = 0.5) {
const response = await fetch(irUrl);
const arrayBuffer = await response.arrayBuffer();
const audioBuffer = await audioCtx.decodeAudioData(arrayBuffer);
const convolver = audioCtx.createConvolver();
convolver.buffer = audioBuffer;
convolver.normalize = true;
const dry = audioCtx.createGain();
const wet = audioCtx.createGain();
dry.gain.value = 1 - mix;
wet.gain.value = mix;
return { convolver, dry, wet, connect(source) {
source.connect(dry);
source.connect(convolver);
convolver.connect(wet);
dry.connect(audioCtx.destination);
wet.connect(audioCtx.destination);
}};
}
const reverb = await createReverb(audioCtx, 'ir/church.wav', );
reverb.(source);
3.3 Delay / Echo
const delayNode = audioCtx.createDelay(1.0);
delayNode.delayTime.value = 0.3;
const feedbackGain = audioCtx.createGain();
feedbackGain.gain.value = 0.4;
const wetGain = audioCtx.createGain();
wetGain.gain.value = 0.5;
source.connect(delayNode);
delayNode.connect(feedbackGain);
feedbackGain.connect(delayNode);
delayNode.connect(wetGain);
wetGain.connect(audioCtx.destination);
const delayL = audioCtx.createDelay(1.0);
const delayR = audioCtx.createDelay(1.0);
delayL.delayTime.value = 0.25;
delayR.delayTime.value = 0.5;
const feedbackL = audioCtx.createGain();
const feedbackR = audioCtx.createGain();
feedbackL.gain.value = 0.3;
feedbackR.gain. = ;
source.(delayL);
source.(delayR);
delayL.(feedbackL);
feedbackL.(delayR);
delayR.(feedbackR);
feedbackR.(delayL);
delayL.(audioCtx.);
delayR.(audioCtx.);
3.4 Distorsion (WaveShaperNode)
function createDistortionCurve(amount = 50) {
const samples = 44100;
const curve = new Float32Array(samples);
for (let i = 0; i < samples; i++) {
const x = (i * 2) / samples - 1;
curve[i] = ((3 + amount) * x * 20 * Math.PI / 180) /
(Math.PI + amount * Math.abs(x));
}
return curve;
}
const distortion = audioCtx.createWaveShaper();
distortion.curve = createDistortionCurve(100);
distortion.oversample = '4x';
const gainBefore = audioCtx.createGain();
gainBefore.gain.value = 1.0;
const gainAfter = audioCtx.createGain();
gainAfter.gain.value = 0.5;
source.connect(gainBefore);
gainBefore.connect(distortion);
distortion.connect(gainAfter);
gainAfter.connect(audioCtx.);
3.5 Compression (DynamicsCompressorNode)
const compressor = audioCtx.createDynamicsCompressor();
compressor.threshold.value = -24;
compressor.knee.value = 30;
compressor.ratio.value = 4;
compressor.attack.value = 0.003;
compressor.release.value = 0.25;
source.connect(compressor);
compressor.connect(audioCtx.destination);
4. Analyse Temps Réel
4.1 Analyseur de spectre (AnalyserNode)
const analyser = audioCtx.createAnalyser();
analyser.fftSize = 2048;
analyser.smoothingTimeConstant = 0.8;
analyser.minDecibels = -90;
analyser.maxDecibels = -30;
source.connect(analyser);
analyser.connect(audioCtx.destination);
const bufferLength = analyser.frequencyBinCount;
const frequencyData = new Uint8Array(bufferLength);
function updateSpectrum() {
analyser.getByteFrequencyData(frequencyData);
const canvas = document.getElementById('spectrum');
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#00ff00';
const barWidth = canvas.width / bufferLength;
for (let i = 0; i < bufferLength; i++) {
const barHeight = (frequencyData[i] / 255) * canvas.height;
ctx.fillRect(i * barWidth, canvas. - barHeight, barWidth - , barHeight);
}
(updateSpectrum);
}
waveform = (bufferLength);
analyser.(waveform);
4.2 Détection de pitch (autocorrélation)
function autoCorrelate(buffer, sampleRate) {
const SIZE = buffer.length;
let maxSamples = Math.floor(SIZE / 2);
let bestOffset = -1;
let bestCorrelation = 0;
let rms = 0;
for (let i = 0; i < SIZE; i++) {
const val = buffer[i];
rms += val * val;
}
rms = Math.sqrt(rms / SIZE);
if (rms < 0.01) return -1;
let lastCorrelation = 1;
for (let offset = 0; offset < maxSamples; offset++) {
let correlation = 0;
for (let i = 0; i < maxSamples; i++) {
correlation += Math.abs((buffer[i]) - (buffer[i + offset]));
}
correlation = 1 - (correlation / maxSamples);
if (correlation > 0.9 && correlation > lastCorrelation) {
bestOffset = offset;
bestCorrelation = correlation;
}
lastCorrelation = correlation;
}
if (bestOffset > ) {
sampleRate / bestOffset;
}
-;
}
() {
analyser.(timeDomain);
pitch = (timeDomain, audioCtx.);
(pitch > ) {
note = (pitch);
.(). = ;
}
(updatePitch);
}
() {
notes = [, , , , , , , , , , , ];
semitone = * .(freq / ) + ;
octave = .(semitone / ) - ;
noteIndex = .(semitone) % ;
;
}
5. Capture Audio (Microphone)
5.1 Entrée microphone
async function startMic(audioCtx) {
try {
const stream = await navigator.mediaDevices.getUserMedia({
audio: {
sampleRate: 48000,
channelCount: 1,
echoCancellation: true,
noiseSuppression: true,
autoGainControl: false
}
});
const source = audioCtx.createMediaStreamSource(stream);
const analyser = audioCtx.createAnalyser();
source.connect(analyser);
return { stream, source, analyser };
} catch (err) {
console.error('Erreur micro:', err);
}
}
const mic = await startMic(audioCtx);
5.2 Enregistrement
class AudioRecorder {
constructor(audioCtx, source) {
this.ctx = audioCtx;
this.source = source;
this.chunks = [];
this.mediaRecorder = null;
}
start() {
const dest = this.ctx.createMediaStreamDestination();
this.source.connect(dest);
this.mediaRecorder = new MediaRecorder(dest.stream, {
mimeType: 'audio/webm;codecs=opus'
});
this.mediaRecorder.ondataavailable = (e) => {
if (e.data.size > 0) this.chunks.push(e.data);
};
this.mediaRecorder.start(100);
}
stop() {
return new ( {
.. = {
blob = (., { : });
. = [];
(blob);
};
..();
});
}
() {
arrayBuffer = blob.();
audioCtx.(arrayBuffer);
}
}
6. Tone.js (Framework haut niveau)
6.1 Installation et base
import * as Tone from 'tone';
await Tone.start();
console.log(`Tone.js ready, sample rate: ${Tone.context.sampleRate}`);
const synth = new Tone.Synth().toDestination();
synth.triggerAttackRelease('C4', '8n');
const polySynth = new Tone.PolySynth(Tone.Synth).toDestination();
polySynth.triggerAttackRelease(['C4', 'E4', 'G4'], '4n');
6.2 Synthétiseurs Tone.js
const fmSynth = new Tone.FMSynth({
harmonicity: 3,
modulationIndex: 10,
carrier: { oscillator: { type: 'sine' } },
modulator: { oscillator: { type: 'triangle' } }
}).toDestination();
fmSynth.triggerAttackRelease('A2', '2n');
const amSynth = new Tone.AMSynth().toDestination();
amSynth.triggerAttackRelease('C4', '4n');
const duoSynth = new Tone.DuoSynth({
voice0: { oscillator: { type: 'sawtooth' } },
voice1: { oscillator: { type: 'sine' } }
}).toDestination();
const monoSynth = new Tone.MonoSynth({
oscillator: { type: },
: { : , : },
: { : , : , : , : }
}).();
metalSynth = .({
: ,
: { : , : , : }
}).();
6.3 Séquences et patterns
const notes = ['C4', 'E4', 'G4', 'B4', 'D5'];
const seq = new Tone.Sequence((time, note) => {
synth.triggerAttackRelease(note, '8n', time);
}, notes, '4n');
seq.start('4m');
Tone.Transport.start();
const kick = new Tone.MembraneSynth().toDestination();
const hihat = new Tone.NoiseSynth({ volume: -10 }).toDestination();
const beat = new Tone.Pattern((time, note) => {
if (note === 'kick') kick.triggerAttackRelease('C1', '8n', time);
if (note === 'hat') hihat.triggerAttackRelease('16n', time);
}, ['kick', 'hat', , , , , , ], );
beat.();
part = .( {
synth.(value., value., time, value.);
}, [
{ : , : , : , : },
{ : , : , : , : },
{ : , : , : , : },
]);
... = ;
..();
6.4 Effets Tone.js (chaînés)
const signal = new Tone.Synth();
const reverb = new Tone.Reverb({ decay: 3, wet: 0.5 });
const delay = new Tone.FeedbackDelay('8n', 0.3);
const filter = new Tone.AutoFilter({ frequency: '4n', depth: 0.5 });
const chorus = new Tone.Chorus({ frequency: 0.5, depth: 2 });
const distortion = new Tone.Distortion(0.5);
const compressor = new Tone.Compressor({ threshold: -20, ratio: 4 });
const eq = new Tone.EQ3({ low: 0, mid: 2, : - });
signal.(filter, compressor, distortion, eq, chorus, delay, reverb, .);
signal.(, );
phaser = .({ : , : });
tremolo = .({ : , : }).();
vibrato = .({ : , : });
6.5 Échantillonneurs
const sampler = new Tone.Sampler({
urls: {
'C4': 'C4.mp3',
'D#4': 'Ds4.mp3',
'F#4': 'Fs4.mp3',
'A4': 'A4.mp3'
},
baseUrl: 'https://tonejs.github.io/audio/salamander/',
onload: () => {
sampler.triggerAttackRelease(['C4', 'E4', 'G4'], '4n');
}
}).toDestination();
const players = new Tone.Players({
kick: 'kick.wav',
snare: 'snare.wav',
hat: 'hat.wav',
bass: 'bass.wav'
}).toDestination();
players.player('kick').start();
7. Audio Worklet (Traitement personnalisé bas niveau)
7.1 Module Worklet
class GainProcessor extends AudioWorkletProcessor {
constructor() {
super();
this.gain = 1.0;
this.port.onmessage = (event) => {
this.gain = event.data.gain;
};
}
process(inputs, outputs, parameters) {
const input = inputs[0];
const output = outputs[0];
for (let channel = 0; channel < input.length; channel++) {
const inputChannel = input[channel];
const outputChannel = output[channel];
for (let i = 0; i < inputChannel.length; i++) {
outputChannel[i] = inputChannel[i] * this.gain;
}
}
return true;
}
}
registerProcessor('gain-processor', GainProcessor);
7.2 Utilisation du Worklet
await audioCtx.audioWorklet.addModule('worklet-processor.js');
const workletNode = new AudioWorkletNode(audioCtx, 'gain-processor');
workletNode.port.postMessage({ gain: 0.5 });
source.connect(workletNode);
workletNode.connect(audioCtx.destination);
8. Pitfalls et solutions
| Problème | Cause | Solution |
|---|
| AudioContext suspendu | Interaction requise | Attendre un clic/tauch avant de créer |
| Clics aux notes | Pas de release envelope | Enveloppe ADSR systématique sur chaque note |
| Latence élevée | Tampon trop grand | latencyHint: 'interactive', réduire buffer |
| Distorsion saturée | Somme de signaux > 0dB | Gain master à 0.3, headroom suffisant |
| Feedback Larsen | Boucle micro → HP | Casque, gain micro réduit |
| GC freeze | Nodes non nettoyés | disconnect(), stop(), close() |
| Analyseur trop lent | fftSize trop petit | 2048 minimum, 8192 pour haute résolution |
Références