Музыка, которую сочиняет браузер

· #игра #javascript #звук

Захотелось, чтобы в игре была фоновая музыка, но без файлов и без повторяющейся петли, которая надоедает к третьей партии. В итоге мелодию придумывает сам браузер, каждый раз немного по-новому.

Из чего она собрана

  • Бас ходит по четырём нотам: ля, фа, соль, ми. Каждая звучит один такт.
  • Мелодия берёт ноты из ля-минорной пентатоники. В ней нет полутонов, поэтому любые две соседние ноты звучат прилично, даже если выбраны случайно.
  • Следующая нота мелодии отходит от предыдущей на одну-две ступени вверх или вниз. Без этого получаются скачки через октаву, и это уже не музыка, а будильник.
var PENTA = [0, 3, 5, 7, 10, 12, 15, 17];
var BASS = [45, 41, 43, 40];

music.note = clamp(music.note + [-2, -1, 1, 2][Math.floor(Math.random() * 4)], 0, PENTA.length - 1);
toneAt(midi(69 + PENTA[music.note]), beat * 0.9, 'triangle', 0.02, music.next);

Иногда нота пропускается, и в мелодии появляются паузы. Ночью и в грозу пауз больше, а громкость ниже.

Почему не setInterval

Если играть ноты по таймеру JavaScript, ритм плывёт: таймер срабатывает то раньше, то позже, особенно когда браузер занят отрисовкой. Поэтому ноты планируются заранее по часам самого звукового движка, примерно на 150 мс вперёд:

while (music.next < audio.currentTime + 0.15) {
    // запланировать ноту ровно на music.next
    music.next += beat;
}

Функция вызывается каждый кадр игры. Когда игра на паузе, кадров нет, и музыка замолкает сама, отдельно её останавливать не нужно.

Темп

Темп растёт вместе с уровнем, а во время тыгыдыка ускоряется ещё в полтора раза. Получается, что музыка подсказывает, как сейчас идут дела.

По умолчанию музыка выключена: кто открыл сайт на работе, не обрадуется внезапному звуку. Кнопка «музыка» под полем запоминает выбор.