За годы развития браузеры обзавелись множеством API и функциональных возможностей, благодаря которым превратились в невероятно мощные платформы приложений. Яркий пример — это современный веб-синтезатор, подробностями об устройстве которого делимся к старту курса по Fullstack-разработке на Python.
Один из таких API меня особенно заинтересовал — это Web Audio API и его возможности в качестве основы для синтеза аудио в браузере и программирования цифровой обработки сигналов (далее — ЦОС).
С помощью разных современных веб-технологий я создал многофункциональный синтезатор частотной модуляции, который работает полностью в браузере и практически на любом устройстве с выходом в интернет, при этом для всех них используется один и тот же код. Применение веб-технологий и веб-браузера в качестве основы для создания синтезатора позволяет ему мгновенно загружаться, не требовать установки или настройки, безопасно запускаться в «песочнице» браузера и быть эффективным на всех типах устройств с производительностью, близкой к нативной. Конечно, были и препятствия в виде багов/несовместимости браузера и операционной системы, но в целом этот инструментарий оказался очень хорошо продуманным и полезным для создания довольно сложного проекта.
Прежде всего настоятельно рекомендую ознакомиться с интерактивной демоверсией и поиграть на синтезаторе
Ноты проигрываются щелчком/касанием клавиш на экране или нажатием виртуальных клавиатурных клавиш. Чтобы услышать всё многообразие звуков, получаемых при синтезе ЧМ, попробуйте разные пресеты из выпадающего меню слева.
Хотите посмотреть демоверсию на видео или узнать, как создать собственные пресеты с помощью синтезатора? Вот это видео:
Синтез частотной модуляции
Если вы уже знакомы с синтезом ЧМ или вас не интересует эта узкоспециализированная тема аудиосинтеза, переходите сразу к следующей части статьи. Чтобы получить представление о синтезе ЧМ, было бы полезно освоить основные связанные с аудио темы, например осцилляторы. А если вы программист, то наверняка, так же как и я, найдёте очень интересными параллели между синтезом ЧМ и такими темами, как функциональное программирование и теория графов.
Синтез ЧМ впервые появился в конце 1960-х годов. С тех пор он применяется в огромном количестве разнообразных синтезаторов с аппаратно-программным обеспечением. Наверняка вы уже слушали музыку с использованием инструментов на основе синтезатора ЧМ. Это невероятно универсальный и эффективный метод синтеза.
Высота звука определяется его частотой. Осцилляторы выдают звук с фиксированной частотой, выводя звуковые волны определённой формы со скоростью, с которой осциллятор повторяется, определяя частоту. Синтез ЧМ основан на понятии чередования частоты осцилляторов (которые с момента изобретения синтеза ЧМ называются операторами) с помощью другого осциллятора. В зависимости от множества факторов, в том числе частот обоих осцилляторов, соотношения между ними, формы волны осцилляторов и величины модуляции, может воспроизводиться огромное разнообразие различных звуков. Это может напомнить о функциях высшего порядка — действительно, они невероятно похожи.
Вот как выглядит синтез ЧМ на уровне формы волны. Здесь с помощью встроенного в синтезатор осциллографа визуализируется результат осциллятора синусоидальных волн, модулирующего частоту другого такого осциллятора. Причём интенсивность этой модуляции со временем меняется:
Как видите, форма волны растягивается и сжимается по мере увеличения и уменьшения частоты осциллятора в соответствии с выходом модулирующего осциллятора.
Синтез ЧМ довольно недорогой с точки зрения вычислительных затрат — включает лишь простые математические операции с простыми операторами, и все они могут быть очень эффективно выполнены на процессорах. Реализовать его тоже довольно легко: базовый синтез ЧМ может быть выполнен в нескольких десятках строк кода. Есть много сложных математических вычислений, например, вычисление точного влияния синтеза ЧМ на содержание гармоник сигналов и т. д. Но большая их часть, честно говоря, не особо нужна для построения синтезатора ЧМ, предназначенного для создания музыки.
Реализация осцилляторов/операторов
Осцилляторы могут быть реализованы несколькими способами. Метод, используемый в этом синтезаторе ЧМ, заключается в сохранении переменной состояния для фазы (число от 0 до 1, обозначающее процент пройденного осциллятором пути через повторяющуюся форму волны). Фаза растёт от 0 до 1, а затем возвращается к 0. Применяя этот метод, базовый синусоидный осциллятор можно реализовать всего лишь с output = phase * 2π.
Единственное, что ещё потребуется, — обновлять фазу каждого сэмпла осциллятора согласно его частоте. При обработке звука выдаётся фиксированное количество сэмплов (со значениями от –1 до 1, которые представляют звуковые волны) в секунду. 44 100 сэмплов в секунду — это стандартное значение, которое применяется и в этом синтезаторе. Учитывая эти факты, получаем функцию для обновления каждого сэмпла фазы осциллятора:
fn update_phase(&mut self, frequency: f32) {
// 1 phase corresponds to 1 period of the waveform. 1 phase is passed every (SAMPLE_RATE /
// frequency) samples.
let mut new_phase = (self.phase + (1. / (SAMPLE_RATE as f32 / frequency))).fract();
if new_phase < 0. {
new_phase = 1. + new_phase;
}
self.phase = new_phase;
}
Заметили особую обработку отрицательных фаз? Это побочный эффект синтеза ЧМ: иногда могут создаваться отрицательные частоты. К счастью, мы можем просто переместить фазу не вперёд, как обычно, а назад — и всё работает. Ещё одно соображение — это так называемая передискретизация, то есть метод, используемый для уменьшения эффекта наложения. Наложение вызвано тем фактом, что мы квантуем звук на частоту дискретизации. Нежелательный эффект может проявляться в нежелательных звуковых артефактах. Но это лишь деталь реализации, и она не влияет на математические вычисления. Если вам интересно, есть много ресурсов, в которых очень подробно показываются принцип работы и особенности реализации.
Исходя из того, как реализуются осцилляторы, реализация синтеза ЧМ, используемая в этом синтезаторе, — это, в принципе, фазовая модуляция. Но на самом деле она эквивалентна синтезу ЧМ и имеет тот же результат.
Продвинутый синтез ЧМ
Синтез ЧМ становится особенно интересным, когда множество операторов объединяются в графы. В этих графах могут быть цепи обратной связи, где оператор A модулирует оператор B, который, в свою очередь, модулирует оператор A, или операторы могут даже модулировать себя напрямую. У каждого оператора может быть любая базовая частота, которая затем можно модулировать другими операторами.
Обычно целочисленные доли базовой частоты (2/1, 3/2, 4/1 и т. д.) звучат лучше всего. Добавьте сюда способность этих операторов степенями модуляции модулировать друг друга (это называется индексами модуляции) и изменяться с течением времени, и вы увидите, как создаётся практически безграничное многообразие воспроизводимых тонов, тембров и стилей звука.
Настраивая эти параметры и подключая операторы, можно воспроизвести всё, что угодно, — от самого изящного электропианино до самой жёсткой басовой партии с дабстепом. Один из недостатков этого огромного пространства параметров — то, что синтез ЧМ может быть очень восприимчивым даже к небольшим изменениям. Это свойство присуще многим сложным системам, и я обнаружил, что из-за него использование синтеза ЧМ может стать очень трудным делом, полным нюансов.
Чтобы визуализировать граф синтеза ЧМ и управлять им как можно точнее, в этом синтезаторе ЧМ используется modulation matrix_:
Каждая клетка матрицы соответствует индексу модуляции между одним и другим операторами. В синтезаторе поддерживается установка константных значений, при этом значение предоставляется из генератора огибающей (подробнее о нём ниже) или является умножителем частоты воспроизводимой в данный момент ноты. Это обеспечивает очень точный контроль над звуком.
В этом видео показан пример синтеза ЧМ с двумя операторами. Второй оператор модулирует первый, при этом индекс модуляции (её степень) меняется вручную перетаскиванием ползунка. Вы можете услышать, как изменение умножителя частоты модулирующего оператора влияет на получающийся в итоге звук.
Здесь мы добавляем ещё один оператор, так что оператор 3 модулирует оператор 2, который модулирует оператор 1:
И это лишь верхушка айсберга звуков, создаваемых синтезом ЧМ!
На случай если вы хотите узнать больше, я написал по матрице модуляции синтезатора более подробное руководство, рассчитанное больше на тех, кто хочет самостоятельно использовать синтезатор.
Техническая реализация и связующее ПО / подключение
Как уже говорилось выше, в этом синтезаторе ЧМ используются современные веб-технологии и самые разные API. Теперь расскажем подробно о них и об их роли в построении синтезатора.
WebAssembly
Основной механизм синтеза написан на Rust и скомпилирован в WebAssembly. Эту комбинацию я активно использовал в прошлом и считаю её невероятно универсальной и эффективной. Для визуализации аудио требуются очень согласованные характеристики производительности, чтобы избежать пропуска сэмплов. В отличие от пропуска кадра в графическом программировании, где это почти наверняка останется незамеченным, пропуск кадра визуализации аудио почти всегда слышен — как щелчок или хлопок. Генерирование мусора из JS требует от сборщика выполнение прохода в точке, где есть высокая вероятность возникновения пропуска кадра, особенно на недорогих устройствах. Превосходные характеристики производительности Rust + Wasm идеальны для этой ситуации, а написание кода ЦОС на Rust удобно и естественно.
А ещё здесь доступно большое количество высококачественных современных инструментов. Вдобавок к отличной документации, активным основным участникам проекта и хорошо продуманным интерфейсам почти все эти инструменты очень легко создавать и устанавливать с нуля. Вот инструменты, которые я использовал для создания этого синтезатора:
wasm-opt and wasm2wat из Binaryen. В оба этих инструмента в качестве входных данных принимаются скомпилированные модули Wasm, причём для них подходят Wasm, созданные с помощью любого языка или инструментария:
С wasm-opt в уже скомпилированных модулях Wasm выполняются разные оптимизации, отлично дополняющие оптимизации языкового уровня Rust/LLVM. При этом размер модуля Wasm часто может уменьшаться более чем на 20% по сравнению с уже оптимизированными модулями.
С wasm2wat скомпилированные модули Wasm преобразуются в текстовый формат WebAssembly, позволяя увидеть, какой именно код сгенерирован и какие функции создаются.
twiggy — это профайлер кода Wasm, в котором очень чётко показывается, какие функции и сегменты кода занимают в модуле Wasm больше места. Размер главного модуля Wasm синтезатора ЧМ — 27 Кб по сети после сжатия: в нём много кода для таблично-волнового синтеза, который в этом синтезаторе не применяется. Twiggy позволяет невероятно легко обрезать модули, избавляясь от лишнего кода.
wasm-bindgen — отличный инструмент для оптимизации процесса встраивания Rust в Wasm и генерации биндингов JavaScript/TypeScript.
В самом Rust тоже есть отличная поддержка компиляции в WebAssembly на уровне языка — даже для такого новейшего функционала Wasm, как ОКМД (одиночный поток команд, множественный поток данных).
ОКМД Wasm
В WebAssembly недавно добавили поддержку инструкций ОКМД фиксированной ширины, позволяющую использовать ОКМД на любом процессоре с поддержкой ОКМД, независимо от архитектуры или набора функций и с одним и тем же кодом. В синтезаторе ЧМ я использую ОКМД, если в браузере есть поддержка ОКМД (пока только в Google Chrome: я зарегистрировался в Origin Trial).
В Firefox она предоставляется после включения в настройках. Когда поддержка Wasm ОКМД недоступна, загружается резервный код. Обратите внимание: это единственное место в приложении, где я пишу другой код для обеспечения совместимости системы, и это всего лишь оптимизация, которая нужна только потому, что во многих браузерах ещё не добавлена поддержка Wasm ОКМД.
Основной способ его использования в синтезаторе — это обработка массивов сэмплов, параметров и других значений с плавающей запятой. В коде есть много мест, где буферы f32 нужно: 1) копировать, 2) умножать на месте на константу или другие значения или 3) инициализировать константным значением. В Wasm ОКМД на Rust поддерживаются все эти операции с типизированными интринсиками на уровне языка. Обратите внимание: некоторые из этих вариантов применения было бы лучше заполнить операциями с большим объёмом памяти WebAssembly, но я не использую их в синтезаторе.
Web Audio
Web Audio — это основание, на котором строится синтезатор. Здесь обрабатываются все платформо-зависимые части самого низкого уровня построения синтезатора, включая:
нормализацию частоты дискретизации системы до заданной пользователем частоты;
определение и запуск звукового графа и передачу звука в аудиодрайверы и на устройства вывода;
микширование нескольких каналов аудио;
предоставление разных узлов для усиления, ограничения/сжатия, фильтров и даже осцилляторов (хотя в этом синтезаторе они не используются);
запуск пользовательского кода ЦОС в выделенном потоке визуализации аудио, чтобы избежать выпадений и сбоев, вызванных нехваткой ресурсов в основном потоке визуализации.
Web Audio был очень кстати при создании синтезатора, тем более что мой опыт обработки аудио невелик. Учитывая огромное количество всевозможных аудиодрайверов и аппаратного обеспечения, а также требований по визуализации аудио в реальном времени, с Web Audio мы имеем отличную возможность сосредоточиться на самом приложении и не тратить время на детали реализации в конкретной системе. Это одно из самых больших преимуществ веб-программирования, позволяющих понять, почему технологии вроде JavaScript получили такое распространение. Здорово, что сфера их применения включает и обработку звука.
AudioWorkletProcessor
Весь код генерации аудио в синтезаторе реализован в WebAssembly и выполняется в потоке визуализации аудио благодаря AudioWorkletProcessor. Интерфейсы AudioWorkletProcessor (или AWP, как я часто называю их в коде и документации) позволяют программистам определять полностью пользовательский код ЦОС для визуализации аудио и даже поддерживают компиляцию, инстанцирование и запуск модулей WebAssembly. Исходный код для AWP этого синтезатора находится здесь.
Если вас интересуют подробности создания AWP на WebAssembly, вот ещё одна статья с детальным описанием.
Есть три способа передачи данных между интерфейсами AWP в потоке визуализации аудио и основным потоком пользовательского интерфейса, где выполняется остальная часть кода:
Звуковой граф через буферы ввода/вывода и параметры.
Порт сообщений.
SharedArrayBuffer.
Все эти три способа имеют свои преимущества и применяются в разных частях реализации синтезатора. Буферы вывода используются для основного сгенерированного аудио от синтезатора, по одному на каждый канал. Они подключены к огибающим усиления и к фильтру (если включён), по одному на каждый голос. Этот синтезатор полифонический, то есть одновременно может воспроизводиться несколько нот. В одном голосе воспроизводится одна нота и имеются собственная базовая частота, собственная огибающая усиления и фильтр.
Порты сообщений нужны, чтобы в голосах начиналось или останавливалось воспроизведение в момент, когда пользователи нажимают клавиши и обрабатывают другие события пользовательского интерфейса, например для настройки эффектов, изменения индексов модуляции, настройки формы волны осцилляторов. Кроме того, они используются во время процесса инициализации для отправки большого двоичного объекта Wasm в AWP, так как выполнять сетевые запросы из потока визуализации аудио невозможно.
Наконец, SharedArrayBuffer применяется для получения из аудиопотока данных в реальном времени относительно текущей фазы всех генераторов огибающей.
Генераторы огибающей
Одна из самых важных частей синтезатора для придания динамичности и характера его звучанию — это генераторы огибающей. Они похожи на осцилляторы выводом меняющегося со временем значения, но отличаются от них поддержкой очень тонкой настройки формы, длины, величины и зацикливаемых свойств генерируемых сигналов.
Хотя в Web Audio предусмотрен набор примитивов для создания генераторов огибающей, в Firefox есть очень серьёзные неустранённые баги, которые делают их неработоспособными. Кроме того, созданием собственных генераторов огибающей (вот исходный код) обеспечивается гораздо более тесная интеграция в сам синтезатор. Я могу визуализировать и считывать из сгенерированных огибающих собственный встроенный код синтезатора без необходимости подключать их к звуковому графу и копировать буферы в Wasm и из Wasm.
В синтезаторе генераторы огибающей могут использоваться для управления/модуляции множества различных параметров синтезатора, в том числе индексов модуляции, выходных уровней оператора, всех параметров эффекта, частоты среза фильтра и других. Кроме того, их можно зациклить, чтобы точнее имитировать осцилляторы, так, чтобы звук синтезатора продолжал меняться и трансформироваться ещё долго после нажатия клавиши. Они позволяют добавить звучанию синтезатора совершенно новые возможности. Для всех желающих узнать больше о генераторах огибающей этого синтезатора и о том, как их использовать, я написал подробную документации.
На изображении выше жёлтым обозначены область, в которой находится на текущий момент генератор огибающей, и выводимое им сейчас значение. Чтобы показать его, нужно получить эти данные прямо из потока визуализации аудио, где вычисляются и с как можно меньшей задержкой отображаются ADSL-огибающие. Для этого используется SharedArrayBuffer — это веб-API с поддержкой параллельного доступа к буферу между несколькими потоками — как раз то, что нам нужно.
Для этого варианта применения не требуется никакой атомарности или другой синхронизации. Здесь создаётся SharedArrayBuffer, и ссылки на него делятся между обоими потоками — основным потоком пользовательского интерфейса и потоком визуализации Web Audio. Затем, когда с помощью AudioWorkletProcessor в синтезаторе ЧМ визуализируется кадр, сюда отправляется текущая фаза всех генераторов огибающей, которые записываются в буфер. А дальше — в пользовательском интерфейсе генератора огибающих эти значения из каждого кадра цепочки воспроизведения буфера считываются и используются для определения ширины обозначенной области.
Визуализации аудио
Я не упомянул о многих частях этого синтезатора, таких как интеграция WebMIDI, управление полифоническими голосами, оптимизация отбрасывания оператора/голоса и встроенные аудиоэффекты. Единственное, о чём хотелось бы обязательно рассказать, — это визуализации аудио. В синтезатор я включил две из них: осциллограф и спектрограмму. В них звук визуализируется во временной и частотной областях соответственно. В обеих визуализациях можно видеть, что именно делается в синтезаторе и какие составляющие приводят к тому, что вы слышите.
В Web Audio для этого есть AnalyserNode, в котором выполняется вся тяжёлая работа. Здесь, чтобы получить используемое в спектрограмме представление частотной области, к аудио и сигналу с помощью оптимизированного собственного кода применяется быстрое преобразование Фурье и для данных временной области прямо из аудиографа извлекаются сэмплы.
Осциллограф
Использованный мной осциллограф невероятно прост, в его основе лежит библиотека Wavy Jones. Это самая простая визуализация аудио, в ней выходные сэмплы отображаются в виде линейного графика. Тем не менее это полезный инструмент, позволяющий увидеть, что именно воспроизводится в синтезаторе.
Спектрограмма
Спектрограмма немного сложнее. Весь звук и фактически все сигналы состоят из множества синусоидных волн разных частот и интенсивности. С помощью быстрого преобразования Фурье звук из массива сэмплов преобразуется в массив значений частоты. Эти значения частоты затем можно нанести на тепловую карту и увидеть, какие частоты преобладают в звуке и как они меняются со временем.
Суть в этом, но в реализации спектрограммы этого синтезатора есть несколько разных функций настройки цветов и поддержка масштабирования значений как в линейном, так и в экспоненциальном масштабе — для фильтрации шума.
Заключение
Создавая синтезатор, я стал настоящим фанатом синтеза ЧМ. Сначала меня сразила элегантность алгоритма и параллели с известными мне концепциями программирования. А по мере того как синтезатор обретал форму и добирал функциональность, я находил всё больше и больше островков параметров, с помощью которых создавались удивительные звуки.
Кроме того, моя вера в возможности и полезность современного веб-браузера и интернета как платформы приложений становится ещё сильнее, чем раньше. Так многого можно добиться, выложив контент в виде сайта вне релизов конкретной ОС, магазинов приложений или проприетарных библиотек и наборов инструментов. С каждым годом мне как разработчику очевидно: разница в возможностях веб-приложений и нативных приложений только сокращается. Радостно видеть, как всё это продолжает набирать обороты. Ведь эти технологии превращаются из новейших нишевых разработок в технологическую основу нашего мира.
А мы поможем вам прокачать навыки или освоить профессию, которая будет востребована в любое время:
Выбрать другую востребованную профессию.
Краткий каталог курсов и профессий
Data Science и Machine Learning
Python, веб-разработка
Мобильная разработка
Java и C#
От основ — в глубину
А также
AnthonyMikh
Ок, м-да.
Термин SIMD на практике используется как есть, без перевода.