Неортодоксальный пример реактивного программирования, связанный с музыкой, с примерами кода и подробным объяснением.

Фон Франьо, Frontend-разработчик @comsysto

Введение

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

Текст предполагает наличие некоторых знаний о реактивном программировании, предпочтительно RxJS.

Как играть полиритмию?

Из Википедии: «Полиритмия — это одновременное использование двух или более конфликтующих ритмов». Возьмем в качестве примера полиритмию 3 против 4: полиритм 3/4 — это когда вы слышите 3 равномерно расположенных импульса за то же время, что и 4 равномерно расположенных импульса (и эта последовательность повторяется). Если повторяющийся цикл длится 1 секунду, один импульс будет слышен в: 0 секунд, 1/3 секунды, 2/3 секунды (каждую 1/3 секунды). Второй импульс будет слышен через 0 секунд, 1/4 секунды, 2/4 секунды и 3/4 секунды (каждую 1/4 секунды).

Простой способ добиться этого программно — разделить цикл на 12 интервалов (3*4=12) и иметь часы, генерирующие числа в начале каждого интервала (если цикл равен 1 секунде, интервал будет длиться 1/12 секунды). Затем мы можем брать эти числа по мере их создания, и если число делится на 3 или 4, мы воспроизводим звук.‍

Как играть полиритмию, если ты робот

‍В таблице представлен один цикл полиритма 3/4. Возрастающие числа (вторая строка) генерируются через равные промежутки времени (первая строка). 3-я и 4-я строки показывают, когда воспроизводится звук. Т.е., в момент времени t=3/12 с издается число 3, и, поскольку 3 делится на 3, воспроизводится звук (представленный X).

Программа

Посмотреть и отредактировать код можно онлайн здесь.

Репозиторий находится здесь.

Начнем с конца: вот как должно выглядеть приложение и что оно должно делать.

Краткое описание особенностей:

  1. Мы можем ввести наши ритмы в 2 входа
  2. Мы можем ввести BPM-ударов в минуту (BPM — это стандартная единица измерения в музыке).
  3. У нас есть кнопка воспроизведения/остановки
  4. Когда играет ритм, мы хотим иметь возможность изменять любой из этих параметров на лету.

Давайте посмотрим на реализацию, шаг за шагом.

1

Мы хотим реагировать на изменения в элементах ввода и на нажатия кнопок. Давайте создадим последовательности Observable из этих событий:

Последовательность ритмов немного сложнее — мы начинаем с массива наблюдаемых последовательностей, по одной на каждый вход (rhythmChangeEvents$). Оттуда мы используем оператор combineLatest, поэтому мы получаем только одну последовательность, которая генерируется каждый раз, когда изменяется входное значение. Он выдает массив входных значений.

2

Нам нужен Observable, испускающий логические значения, которые могут служить сигналом для воспроизведения/остановки звука. Для этого мы будем использовать BehaviorSubject:

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

3

Затем мы объединяем 3 последовательности в 1, которая должна управлять «часами» Observable (то есть генерировать числа через равные промежутки времени).

Вот последовательности:

  1. rhythm$ — влияет на тактовую частоту и звукорежиссеры.
  2. bpmChanges$ — влияет на тактовую частоту (интервал)
  3. play$ — запускает/останавливает часы

Таким образом, config$ observable генерирует объект массива каждый раз, когда генерируется любой из отдельных Observables.

4

Далее, каждый раз, когда config$ создает новое значение, нам нужно реагировать на это значение. Есть два потока:

  • config.play имеет значение false — значение сигнализирует о том, что мы должны остановиться, поэтому мы сопоставляем это значение с новой последовательностью Observable, которая никогда не испускается («НИКОГДА»)
  • config.play истинно — мы сопоставляем значение с новой наблюдаемой последовательностью (часы). Интервал задается на основе BPM и ритмов (расчет производится в функции convertToMs).

В этом случае удобен оператор switchMap, потому что он выдает значения только из самого последнего отображенного (проецируемого) Observable. Другими словами, когда генерируется новый объект конфигурации, switchMap прекращает генерировать последнюю последовательность часов и начинает генерировать только что созданную.

В операторе tap мы перехватываем сгенерированное значение конфигурации и при необходимости меняем текст кнопки.

5

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

Чтобы воспроизвести ритм, нам нужен доступ к ритму$ и к часам$. Опять же, мы используем combineLatest для создания Observable, который испускает составной объект. Для каждого ритма мы выполняем функцию («саунд-продюсер»). Функция проверяет, делится ли число (часы) на число «ритма» и воспроизводит звук.

Звук воспроизводится с помощью HTML5 audio API. Эта функция просто берет образец, доступный в Интернете, и воспроизводит его.

ДОКУМЕНТАЦИЯ

Документация для всех используемых операторов:

объединитьПоследние

"кран"

переключить карту

интервал

от события

"Карта"

Первоначально опубликовано на comsystoreply.de.