В 1946 году Лео Фендер разработал и создал первую в мире электрогитару The Broadcaster, потрясающий и новаторский инструмент, навсегда изменивший мир ладовых инструментов. Перенесемся в сегодняшний день… В 2015 году Fender запустил цифровую команду для создания нашего флагманского музыкального образовательного приложения Fender Play. Мы делаем несколько крутых технических вещей, чтобы лучше учиться у новых игроков!

Наша последняя функция - это то, что мы называем режимом практики. Тренировочный режим - это интерактивная игра подыгрыша, которая направляет игроков вместе с песней в реальном времени, с настоящим метрономом и с высококачественными минусовками.

Я знаю, о чем вы думаете ... В реальном времени? JavaScript? В Интернете?

да. Всем этим. Мы делаем довольно интересные вещи с WebAudio API, работниками, requestAnimationFrame, MusicXML и TypeScript для достижения всех этих целей. (Примечание: режим практики был построен на 100% TypeScript - любое использование термина «JavaScript» может быть идентично заменено на «TypeScript»)

Fender Play: музыкальная перспектива

Значительную часть нашей команды разработчиков программного обеспечения Fender составляют музыканты - талантливые музыканты. Несмотря на это, до запуска режима практики теория музыки практически не касалась нашей кодовой базы. Единственное место, где вы могли удаленно найти теорию музыки, было в нашем средстве визуализации табулатур. Средство визуализации было довольно простым по функциональности, получая данные MusicXML в качестве входных данных и выводя один компонент табулатуры SVG.

Для негитаристов, читающих это, табулатура для гитары (или табулатура для краткости) - очень популярная альтернатива чтению традиционных нот. Упрощенные обозначения упрощают обучение инструкторов, а игрокам - обучение.

Отзывы пользователей и концепция режима практики

Как и большинство современных компаний-разработчиков программного обеспечения, мы практикуем гибкую разработку. Чтобы улучшить наш продукт, мы регулярно собираем отзывы пользователей из таких торговых точек, как Intercom и обзоры App Store. Мы получили запрос номер один: мы хотим, чтобы вкладки автоматически прокручивались в реальном времени. Это подтолкнуло нас к концепции Практического режима.

В основе режима практики лежат две концепции: (1) отслеживание индивидуального прогресса в учебе и (2) компонент пользовательского интерфейса, называемый листами практики. Для релевантности мы будем говорить только о листах практики.

К практическим листам предъявлялись следующие требования:

  1. По возможности повторно используйте существующее средство визуализации табулатур.
  2. Создайте визуальный индикатор (указатель воспроизведения), который перемещается горизонтально вместе с вкладкой в ​​реальном времени.
  3. Вертикальная автопрокрутка страницы по мере продвижения вкладки.
  4. Обеспечьте звуковой метроном с регулируемой / увеличивающейся скоростью.
  5. Обеспечьте расширяемость для будущих добавлений функций, таких как программные инструменты, MIDI и звуковые дорожки.

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

Поговорим о (программных) часах: самое время !!!

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

Природа практических листов и теории музыки потребовала от нас создания продукта, который мог бы точно анимировать музыку в течение определенного периода времени. В то время как такие языки, как C ++, предлагают решения для обеспечения обработки в реальном времени, JavaScript этого не делает, по крайней мере, на поверхности.

Давайте начнем с изучения этого примера фрагмента JS:

Таймер когда-нибудь истекает? Записывается ли когда-нибудь журнал в консоль? Если вы ответили «да» на оба эти вопроса, подумайте еще раз.

Встроенные временные операции (а именно setTimeout и setInterval) не гарантированно будут точно рассчитаны по времени, поскольку таймеры JS ждут до следующего цикла событий. Таким образом, предоставленное время следует рассматривать как минимальное время до запуска обработчика. В приведенном выше примере setTimeout ожидает следующего цикла событий, но бесконечный цикл никогда не допускает возникновения будущего цикла событий. Таким образом, хотя мы ожидаем, что таймер немедленно истечет, на самом деле этого никогда не происходит.

Любой здравомыслящий разработчик сначала попробует использовать setTimeout для планирования событий, зависящих от времени. Мы сделали. Упрощенный прототип выглядел примерно так:

Мы также пробовали аналогичное решение, в котором мы регулярно опрашивали обновления с помощью setInterval. Теоретически оба этих решения должны работать без сбоев. В большинстве приложений - даже с потенциальными задержками - обычно достаточно setTimeout и setInterval. Однако в музыке, где ритм и темп привязаны к определенной сетке, такое решение неприемлемо.

Проблема с этим очевидна. Запланированное воспроизведение звука с помощью WebAudio API в реальном времени, а анимация JavaScript - нет. Это означает, что любая задержка цикла событий JS - сборка мусора, рендеринга / макетов или другого кода приложения - может легко вызвать несинхронизированный беспорядок между анимацией и сгенерированным звуком метронома.

Итак ... вы использовали _8 _ / _ 9_, и у вас наблюдаются странные задержки анимации и несоответствующий звук. Оказывается, вы все еще можете использовать эти встроенные функции таймера… только не в одиночку.

Наше решение для достижения синхронизации, близкой к реальному времени, включало объединение не одного, не двух, а ТРЕХ разных часов:

  1. WebAudio: audioContext.currentTime - «главные» часы
  2. Рабочий поток с setInterval() опросом с фиксированной скоростью
  3. Частота графического процессора + частота основного потока JS: requestAnimationFrame()

Так зачем нам каждые часы?

API и часы WebAudio работают на аппаратном уровне - все операции делегируются аппаратному обеспечению. Вы можете постоянно проверять «текущее время», получая audioContext.currentTime. Мы также можем запланировать любое воспроизведение звука на эти часы. Очень распространенная практика - ставить в очередь события, основанные на времени, вне системных часов, например ,Date.now(), но системные часы могут быть произвольными. Поскольку планирование событий связано с фиксированными часами, вы можете использовать любые часы. Поскольку мы планируем аудио, почему бы не запланировать события анимации относительно audioContext.currentTime? Часы WebAudio становятся нашими главными часами и гарантируют аудиовизуальную синхронизацию.

Мы использовали вызов setInterval() в рабочем потоке. Поскольку единственная работа в нашем работнике - setInterval() (и периодическая передача сообщений), мы можем гарантировать точно рассчитанные интервалы времени. На каждом такте рабочего цикла мы ставим в очередь событие анимации / звука в основном потоке. Если в основном потоке возникают задержки цикла событий JS, он просто ставит в очередь заблокированные обработчики worker.onTick, а затем вызывает их последовательно - гораздо лучшая альтернатива, чем смещение всех будущих интервалов.

requestAnimationFrame() работает, предоставляя обратный вызов, который запускается перед каждой перерисовкой. Затем мы можем обработать нашу очередь анимации в зависимости от потребности - обрабатывать анимацию только до обновления кадра. Это полностью устраняет необходимость в setInterval или цикле while для обработки очереди и снижает риск недостаточного или избыточного опроса.

Расширяемость и архитектура: соединяем части

Итак, мы знаем о часах и базовой логике анимации, но как все это соединяется воедино? Как на самом деле работают листы с практическими занятиями?

На высоком уровне наш компонент пользовательского интерфейса, практические листы, создает экземпляр объекта AudioContext, который передается всезнающему классу с именем PracticeManager. Менеджер создает три объекта с помощью AudioContext: MetronomeScheduler, AnimationDriver и TrackPlayer.

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

Взгляните на следующий метод в PracticeManager. Это демонстрация того, как легко становится добавлять новые плагины:

Когда в октябре 2019 года был запущен режим Практика, в нашей первоначальной дорожной карте не было звуковых минусовок. Из-за расширяемой природы менеджера практики это было относительно легко. Все, что нам нужно было сделать, это реализовать аудиоплеер с основными элементами управления (воспроизведение, пауза, поиск), а остальное по сути было подключи и работай. Совершенно новая функция, но при этом практически нулевой рефакторинг до базовой архитектуры.

И это верно для любого будущего звукового плагина в реальном времени, о котором мы только можем подумать. Программные инструменты, механизмы прослушивания, гитарные эффекты - все это возможно с минимальным вмешательством в исходную архитектуру кода благодаря встроенной системе плагинов PracticeManager. Даже наш планировщик метронома является расширяемым и может быть повторно использован для других приложений пользовательского интерфейса на основе звука!

Давайте погрузимся в архитектуру. Ниже представлена ​​упрощенная схема архитектуры практических листов и основных компонентов, необходимых для работы:

Как упоминалось ранее, мы используем три тактовых генератора для достижения синхронизации в реальном времени между воспроизведением звука и анимации. Когда пользователь начинает играть, запускаются и рабочий поток, и обработчик requestAnimationFrame.

Рабочий запускает setInterval цикл, который дает команду MetronomeScheduler поставить в очередь щелчок метронома, запланированный на часы WebAudio API. Планировщик, в свою очередь, запускает обратный вызов, зарегистрированный AnimationDriver. По сути, AnimationDriver ищет все заметки между последним щелчком метронома в очереди и следующим ожидаемым щелчком. Анимации в очереди планируются относительно как события щелчка метронома, так и ожидаемого времени начала / окончания каждой ноты (опять же, относительно времени звукового контекста).

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

Вспомните, что requestAnimationFrame запускается только тогда, когда это необходимо, то есть всякий раз, когда мы собираемся обновить кадр или когда мы изначально начинаем цикл анимации. Из-за этого мы не тратим бесконечные циклы ЦП на setTimeout() или while циклический опрос. В большинстве сценариев мы легко получаем 60 кадров в секунду в нашей анимации и никогда не пропускаем ни одной доли (буквально), что, на мой взгляд, является очень впечатляющим показателем производительности!

Вывод

Это было очень сложно, но мы хотели поделиться с вами этим открытием. Концепция реального времени в JavaScript для большинства является ересью и богохульством, но при достаточной архитектуре это (в большинстве случаев) возможно.

WebAudio - это мощный API, предназначенный в основном для управления аудиосредствами, но его можно абстрагировать и для других целей. В Fender мы постоянно ищем способы использовать API WebAudio, чтобы сделать наши обучающиеся музыканты еще лучше.

Практические листы - отличное свидетельство того, чего Fender пытается достичь с помощью Fender Play, обеспечивая лучший опыт обучения для наших пользователей. Наш основатель, Лео Фендер, однажды сказал: «Все художники - ангелы, и наша работа - дать им крылья, чтобы летать». Я думаю, что здесь хорошо работает режим «Практика». Мы даем нашим слушателям лучший способ попрактиковаться, и с этого момента все станет только лучше.

Вот гифка с практическими листами в действии!

Особый привет Шону Герберту, ​​инженеру, который работал в POC-режиме и начал с нуля.