Представь задачу: ты разрабатываешь веб-игру для платформы Telegram Mini Apps. Лимит на стартовый размер бандла жесткий — не больше 300–500 килобайт, иначе при слабом мобильном интернете пользователь просто закроет вкладку, не дождавшись загрузки. При этом геймплей завязан на медитативную атмосферу под классические ноктюрны (Шопен, Сати, Дебюсси, Чайковский). Если положить в проект даже сжатые MP3-файлы, бандл мгновенно раздуется до 30–50 мегабайт. А если захочется интерактивности, чтобы звук каждого хода подстраивался под гармонию музыки?
Разработчик Сергей Рожков (@gR3Y) при создании блок-головоломки Blocturne нашел невероятно изящный выход: он отказался от аудиофайлов и построил звуковой движок целиком на базе браузерного Web Audio API.
Я внимательно изучил техническую изнанку этого проекта, и в ней есть масса неочевидных инженерных грабель, с которыми сталкивается любой разработчик сложного веб-аудио.
Ноты вместо байтов: как уместить 19 пьес в 8 килобайт
Вместо того чтобы хранить готовые аудиодорожки, движок оперирует нотами:
- Парсинг MIDI в компактные структуры TypeScript. Машинные партитуры из открытых проектов Mutopia и MuseScore были очищены и нарезаны на музыкальные фразы. Все 19 классических произведений в виде массивов нот и пауз заняли в кодовой базе всего 8 КБ.
- Семплированный банк с интерполяцией. Чтобы не синтезировать плоский звук одними лишь осцилляторами, реальное пианино (семплы Kawai из FreePats) было оцифровано с шагом в малую терцию. Файл моно-AAC занял около 440 КБ. Промежуточные полутона воспроизводятся на лету простым изменением скорости
playbackRateаудиоузла. А пока файл грузится в фоне, игра плавно стартует на легких осцилляторах. - Гармоническая привязка ходов. Звуки действий игрока (установка блока, сгорание линий) извлекают тональность из текущей партии левой руки звучащей пьесы. Ты ставишь фигуру — и щелчок звучит как гармоничная нота в аккорде Шопена, а не как посторонний бип.
Ритмическая магия: квантование времени
Самая большая опасность интерактивного звука — какофония. Если игрок быстро щелкает по экрану невпопад, красивая музыка превращается в шум.
Решением стало квантование времени (Time Quantization). Звук хода не воспроизводится мгновенно по событию pointerdown, а аккуратно выравнивается по сетке 1/32 доли текущего такта (q(32)):
// Вычисление ближайшей 1/32 доли такта после момента now
private q(div: 16 | 32 = 32): number {
const now = this.audioCtx.currentTime;
const stepDuration = this.stepLen() * (16 / div);
const ref = this.files ? this.files.gridRef : this.nextTime;
const k = Math.floor((ref - now) / stepDuration);
// Возвращаем запланированную временную отметку для старта ноты
return Math.max(now, ref - k * stepDuration);
}
Микрозадержка до 70–100 мс человеческим мозгом воспринимается не как лаг ввода, а как невероятно кайфовое ощущение: кажется, будто ты сам виртуозно попадаешь пальцем точно в такт сложной фортепианной пьесы.
А сам щипок струны запускается строго в запланированный квантованный момент:
function playAdaptivePluck(ctx: AudioContext, frequency: number, scheduledTime: number): void {
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = "sine";
osc.frequency.setValueAtTime(frequency, scheduledTime);
// Экспоненциальный спад громкости щипка
gain.gain.setValueAtTime(0.4, scheduledTime);
gain.gain.exponentialRampToValueAtTime(0.0001, scheduledTime + 0.35);
osc.connect(gain);
gain.connect(ctx.destination);
osc.start(scheduledTime);
osc.stop(scheduledTime + 0.35);
}
Засада с нодами на Android и гибридный пререндер
На десктопе в Chrome всё работало безупречно. Но как только игру запустили на бюджетных смартфонах (вроде POCO M4 Pro), начался кошмар: звук заикался, хрипел и захлебывался.
В процессе профайлинга выяснилось: дело не в нехватке процессора. Браузерный движок WebAudio на Android физически не успевал создавать и уничтожать тысячи короткоживущих аудиоузлов (OscillatorNode, GainNode, StereoPannerNode). Сборщик мусора JavaScript (GC) вставал на паузы, срывая непрерывную генерацию звукового буфера. Ранее мы уже сталкивались с тонкостями работы браузерного звука, когда разбирали захват звука в Manifest V3 и перевод единым потоком, и здесь проявились схожие ограничения мобильных рантаймов.
Вместо чистого процедурного синтеза на лету для мобильных устройств был внедрен гибридный подход: перед стартом пьесы фоновый OfflineAudioContext быстро рендерит базовую партию в единый непрерывный буфер, а живой AudioContext воспроизводит только одиночные ноты ходов игрока. Дополнительно тяжелую сверточную реверберацию ConvolverNode заменили легковесной цепочкой из восьми гребенчатых фильтров задержки.
В итоге игра сохранила микроскопический размер, идеальную адаптивность к действиям игрока и получила стабильные 60 FPS даже на слабых телефонах. Это отличный урок: процедурный подход творит чудеса, если помнить об ограничениях мобильных сборщиков мусора.
