Неортодоксальный пример реактивного программирования, связанный с музыкой, с примерами кода и подробным объяснением.
Фон Франьо, 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).
Программа
Посмотреть и отредактировать код можно онлайн здесь.
Репозиторий находится здесь.
Начнем с конца: вот как должно выглядеть приложение и что оно должно делать.

Краткое описание особенностей:
- Мы можем ввести наши ритмы в 2 входа
- Мы можем ввести BPM-ударов в минуту (BPM — это стандартная единица измерения в музыке).
- У нас есть кнопка воспроизведения/остановки
- Когда играет ритм, мы хотим иметь возможность изменять любой из этих параметров на лету.
Давайте посмотрим на реализацию, шаг за шагом.
1
Мы хотим реагировать на изменения в элементах ввода и на нажатия кнопок. Давайте создадим последовательности Observable из этих событий:
Последовательность ритмов немного сложнее — мы начинаем с массива наблюдаемых последовательностей, по одной на каждый вход (rhythmChangeEvents$). Оттуда мы используем оператор combineLatest, поэтому мы получаем только одну последовательность, которая генерируется каждый раз, когда изменяется входное значение. Он выдает массив входных значений.
2
Нам нужен Observable, испускающий логические значения, которые могут служить сигналом для воспроизведения/остановки звука. Для этого мы будем использовать BehaviorSubject:
Мы создаем BehaviorSubject и выдаем из него значения каждый раз, когда нажимается кнопка воспроизведения. При каждом щелчке мы испускаем дополнение к последнему сгенерированному значению.
3
Затем мы объединяем 3 последовательности в 1, которая должна управлять «часами» Observable (то есть генерировать числа через равные промежутки времени).
Вот последовательности:
- rhythm$ — влияет на тактовую частоту и звукорежиссеры.
- bpmChanges$ — влияет на тактовую частоту (интервал)
- 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.