Урок · Середній рівень · ~55 хв
Web Audio API · Синтез · Алгоритмічна композиція

Процедурна музика з Web Audio API

Web Audio API — це повноцінний рушій синтезу: осцилятори, фільтри, обвідні, згорткова реверберація та точна синхронізація. Цей урок будує генератор алгоритмічної музики: тактовий генератор темпу, секвенсор з урахуванням ладу, простий синтезаторний патч з ADSR та рушій акордових послідовностей.

1Утиліти для висоти тону та ладів

// MIDI-нота → частота в Гц const mtof = note => 440 * Math.pow(2, (note - 69) / 12); // Поширені лади як інтервали в півтонах від тоніки const SCALES = { major: [0, 2, 4, 5, 7, 9, 11], minor: [0, 2, 3, 5, 7, 8, 10], pentatonic: [0, 2, 4, 7, 9], blues: [0, 3, 5, 6, 7, 10], dorian: [0, 2, 3, 5, 7, 9, 10], }; // Будуємо всі ноти ладу в N октавах function buildScale(root = 60, scaleName = 'minor', octaves = 3) { const intervals = SCALES[scaleName]; const notes = []; for (let oct = 0; oct < octaves; oct++) { for (const interval of intervals) { notes.push(root + oct * 12 + interval); } } return notes; } const palette = buildScale(48, 'pentatonic', 3); // → [48, 50, 52, 55, 57, 60, 62, 64, 67, 69, 72, 74, 76, 79, 81]

2Синтезаторний патч ADSR

const audio = new AudioContext(); // Головний ланцюг: підсилення → фільтр верхніх частот → лімітер → вихід const masterGain = audio.createGain(); masterGain.gain.value = 0.4; const highpass = audio.createBiquadFilter(); highpass.type = 'highpass'; highpass.frequency.value = 80; masterGain.connect(highpass); highpass.connect(audio.destination); function playNote(freq, startTime, duration, adsr = {}) { const { attack = 0.01, decay = 0.1, sustain = 0.6, release = 0.3 } = adsr; const osc = audio.createOscillator(); const gain = audio.createGain(); const filter = audio.createBiquadFilter(); osc.type = 'sawtooth'; osc.frequency.setValueAtTime(freq, startTime); // Невелике розстроювання для теплоти (два осцилятори з різницею в 5 центів) const osc2 = audio.createOscillator(); osc2.type = 'sawtooth'; osc2.frequency.setValueAtTime(freq, startTime); osc2.detune.value = 5; filter.type = 'lowpass'; filter.frequency.setValueAtTime(200, startTime); filter.frequency.linearRampToValueAtTime(freq * 6, startTime + attack); filter.frequency.exponentialRampToValueAtTime(freq * 2, startTime + attack + decay); gain.gain.setValueAtTime(0, startTime); gain.gain.linearRampToValueAtTime(1, startTime + attack); gain.gain.linearRampToValueAtTime(sustain, startTime + attack + decay); gain.gain.setValueAtTime(sustain, startTime + duration - release); gain.gain.linearRampToValueAtTime(0.0001, startTime + duration); osc.connect(filter); osc2.connect(filter); filter.connect(gain); gain.connect(masterGain); osc.start(startTime); osc.stop(startTime + duration + 0.01); osc2.start(startTime); osc2.stop(startTime + duration + 0.01); }

3Точний генератор з плануванням з випередженням

Web Audio API має точну до семпла синхронізацію, але setInterval «смикається». Розв'яжіть це за допомогою планувальника з випередженням: викликайте JS-колбек кожні 25 мс, але плануйте ноти на 100 мс уперед:

const BPM = 120; const BEAT = 60 / BPM; // секунд на долю const LOOK = 0.1; // вікно випередження (с) const TICK = 25; // інтервал планувальника (мс) let nextNoteTime = 0; let currentBeat = 0; let timerID = null; let playing = false; let onBeat = null; // колбек: (beat, time) => void function scheduler() { while (nextNoteTime < audio.currentTime + LOOK) { if (onBeat) onBeat(currentBeat, nextNoteTime); nextNoteTime += BEAT; currentBeat = (currentBeat + 1) % 32; } timerID = setTimeout(scheduler, TICK); } function startClock() { if (playing) return; audio.resume(); nextNoteTime = audio.currentTime + 0.05; currentBeat = 0; playing = true; scheduler(); } function stopClock() { clearTimeout(timerID); playing = false; }
Цей шаблон узято з канонічної статті Кріса Вілсона про синхронізацію Web Audio. Планувальник ніколи не дрейфує, бо ви завжди плануєте відносно audio.currentTime, а не реального часу.

4Секвенсор мелодії з урахуванням ладу

const notes = buildScale(48, 'pentatonic', 2); // Випадкове блукання: рух на ±1-2 кроки в ладу з переходом через край let pos = Math.floor(notes.length / 2); function nextMelodyNote() { const step = Math.random() < 0.7 ? (Math.random() < 0.5 ? 1 : -1) // найчастіше малий крок : (Math.random() < 0.5 ? 2 : -2); // зрідка стрибок pos = Math.max(0, Math.min(notes.length - 1, pos + step)); return notes[pos]; } // Ймовірність пауз на долю const REST_PROB = 0.15; onBeat = (beat, time) => { if (Math.random() < REST_PROB) return; const midi = nextMelodyNote(); const freq = mtof(midi); const dur = BEAT * (Math.random() < 0.3 ? 2 : 1); // деякі ноти витримуються playNote(freq, time, dur * 0.9, { attack: 0.01, decay: 0.08, sustain: 0.5, release: 0.15 }); };

5Акордові послідовності

// I-VI-IV-V у до мінорі (MIDI-тоніка 60) const PROGRESSIONS = { 'i-VI-III-VII': [[0,3,7], [8,11,3], [3,7,10], [10,1,5]], 'i-iv-v-i': [[0,3,7], [5,8,0], [7,11,2], [0,3,7]], }; const PROG = PROGRESSIONS['i-VI-III-VII']; let chordIndex = 0; function playChord(root, intervals, time) { for (const interval of intervals) { const freq = mtof(root + interval); playNote(freq, time, BEAT * 4, { attack: 0.08, decay: 0.2, sustain: 0.4, release: 0.8 }); } } // Змінюємо акорд кожні 4 долі onBeat = (beat, time) => { if (beat % 4 === 0) { playChord(48, PROG[chordIndex % PROG.length], time); chordIndex++; } // Мелодія звучить зверху if (Math.random() > 0.15) { playNote(mtof(nextMelodyNote()), time, BEAT * 0.85); } };

6Реверберація та ефекти

// Згорткова реверберація з використанням згенерованої імпульсної характеристики async function createReverb(duration = 2, decay = 3) { const rate = audio.sampleRate; const length = rate * duration; const impulse = audio.createBuffer(2, length, rate); for (let ch = 0; ch < 2; ch++) { const data = impulse.getChannelData(ch); for (let i = 0; i < length; i++) { data[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / length, decay); } } const reverb = audio.createConvolver(); reverb.buffer = impulse; return reverb; } // Ефект затримки function createDelay(time = 0.375, feedback = 0.4) { const delay = audio.createDelay(2.0); const fbGain = audio.createGain(); delay.delayTime.value = time; // нота 3/8 при 120 BPM fbGain.gain.value = feedback; delay.connect(fbGain); fbGain.connect(delay); return delay; } // Ланцюг: синтез → затримка → реверберація → masterGain (async () => { const reverb = await createReverb(2.5, 2); const delay = createDelay(BEAT * 0.375, 0.35); masterGain.disconnect(); masterGain.connect(delay); delay.connect(reverb); reverb.connect(audio.destination); })();
Реверберація на основі імпульсної характеристики (convolverNode), створена зі синтетичного шуму, звучить напрочуд природно. Для реальних приміщень завантажте записаний файл IR за допомогою fetch + decodeAudioData.