Урок · Середній рівень · ~55 хв
Процедурна музика з 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.