Punk riff generator

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Когда-то давно, может быть лет пять назад, мне захотелось воспроизвести в браузере звук. Уж не помню, какая конкретно у меня была задача, и чего я хотел добиться — скорее всего просто поиграться с разными семплами; может, запрограммировать трек. Пошел в гугл с вопросом, как это сделать, попал на StackOverflow с вопросом/ответом примерно такого вида. И увидел, как на запрос "playing a simple sound" на меня вываливают какие-то контексты, буферы, декодирование... И так мне стало душно, что я сразу махнул рукой на все это и рассудил, что не сильно-то мне это все и интересно — уж точно не такой ценой. И забыл эту тему на долгие годы.

Как я к этой теме вернулся, я тоже особо не помню. Но мне захотелось воспроизвести в браузере обычный синусоидальный сигнал, а потом в случае успеха как-то поиграться с ним. Возможно, на меня (не)здорово повлияла Hyper Light Drifter с ее chiptune музыкой.

Потом затея стала стихийно развиваться, и в результате случайным образом вылилась в мини-проект "Punk riff generator" или "Генератор панк-риффов", о котором мы в статье и поговорим. В проекте заложена нехитрая идея: дать возможность генерировать и воспроизводить в браузере 4 случайных аккорда, которые можно было бы впоследствии использовать как основу для очередной панк-песни. Ну не здорово ли?

Настраиваемся на прочтение статьи правильно:

  • Статья насчитывает с пару десятков коротких аудио-дорожек, чтобы вы ощутили эволюцию звука, который мы будем творить собственными руками. А эволюция будет, что надо!

  • Повествование будет сопровождаться немного душноватыми спусками в теорию звука, но я попробую объяснять доходчиво

  • Смело предположу, что вам не понадобится никакой музыкальный бекграунд, чтобы понять происходящее в статье. Но это не точно! А на сколько это не точно, мне расскажут в комментариях

  • Статья вышла хитросплетенным миксом из математики, теории музыки и программирования. Читайте и развивайтесь всесторонне :)

Простая синусоида

Итак, мой первый порыв был воспроизвести в браузере синусоидальный сигнал. Сказано-сделано — я как всегда пошел на Stack Overflow, скопипастил оттуда решение, вставил в script-секцию html-страницы и получил желанный сигнал:

Отлично, то что я хотел! Теперь давайте задним числом посмотрим, что за код отвечает за генерацию этого звука. В браузерном JS звуками заведует Web Audio API, и именно его буферы и декодирование отпугнули меня при первой попытке ознакомиться с браузерным аудио некоторое количество лет назад. API действительно достаточно низкоуровневый, а потому, кстати говоря, очень гибкий и с массой возможностей. К самому API мы еще вернемся, он в исходном коде присутствует в большом количестве, но в данном конкретном случае мы код API проигнорируем и сразу обратим внимание на ключевой кусочек кода, который формирует нашу синусоиду:

function sineWaveAt(sampleNumber, tone) {
	const time = sampleNumber / context.sampleRate
	return Math.sin(2 * Math.PI * tone * time)
}

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

Физика (и математика) звука

Все мы слышали, что звук — это колебания, или волна с определенной частотой. Это, конечно, очень абстрактное объяснение, и у нас в голове остается за кадром много вопросов: почему именно волна, а что это конкретно за волна, а можно ли ее увидеть, потрогать?

Почему звук — волна, это в первую очередь вопросы к нашему уху. Барабанная перепонка улавливает колебания, передающиеся по воздуху (или воде) и умеет их интерпретировать, как некоторое ощущение, чувство — то, что мы называем слухом. Поэтому тут скорее справедливее сказать, что не звук — это волна; а волна, дошедшая до уха — это для мозга звук.

Как нам визуализировать такое колебание? Самый простой способ — представить себе график синуса; ну или косинуса, если хотите — они в разрезе нашей задачи не особо отличаются. Синус в каком-то смысле — пример чистейшего звука в вакууме: нота без тембра, окраса, примесей. Попробуем построить каноничную синусоиду y=sin(x):

Скорее всего вам этот график знаком еще со школы, и вы помните, что он бесконечно простирается влево и вправо по оси x — тут у нас идет время в секундах. По оси y — амплитуда колебания волны. Если совсем грубо, то ось y можно считать за громкость сигнала. С этой амплитудой немного сложно, потому что непонятно, с чем соотнести ее величины, так что для простоты давайте будем считать, что если звук лежит в диапазоне [-1;1], то это такой полновесный, эталонный по громкости звук. Буду признателен, если в комментариях кто-нибудь сможет как-то более формально охарактеризовать значения по оси y.

График синусоиды пусть и бесконечен, но у него есть четко выраженный повторяющийся паттерн — период. Для y=sin(x) период равен 2\pi секунды:

И вот здесь с волной y=sin(x) возникает проблема: если одно колебание этого сигнала длится 2\pi \approx 6.28 секунд, следовательно частота такого сигнала равна 1/2\pi\approx0.16Hz. Человек же при этом способен услышать звук лишь в частотном диапазоне от 20Hz до 20000Hz в лучшем случае. Т.е. наш сигнал y=sin(x) — это какой-то экстремальный случай инфразвука, и вряд ли какое-либо живое существо способно его услышать.

Что можно сделать, чтобы волна стала слышимой? Вообще синусоиду можно растягивать по обеим осям, формула синуса для этого слегка обогащается коэффициентами:

y=a*sin(h x)

где

a — скейл по оси y. Чем a больше, тем синусоида выше и, соответственно, громче.
h — скейл по оси x. Чем h больше, тем период синусоиды короче, график сжатее, а частота выше (звук писклявее).

Для того, чтобы наш коэффициент h можно было измерять сразу в герцах (то есть в колебаниях в секунду), чисто для удобства, его неплохо бы сделать кратным 2\pi:

y=a*sin(2\pi h x)

Вот теперь с этим можно работать. Посмотрим на пример с двумя синусоидами:

Что мы можем сказать о этих графиках, имея на руках ту теорию, что мы только что освоили? Сможем ли мы из любопытства подобрать для них точные формулы? Давайте начнем с простого — с амплитуды. Зеленый график имеет вертикальный диапазон [-1; 1], т.е. амплитуда равна единице, прямо как у обычной синусоиды без явных коэффициентов. Синий график сжат по вертикали, и его экстремумы лежат на -0.75 и 0.75, следовательно его a=0.75.

Теперь посмотрим на горизонтальные свойства графиков. Можем обратить внимание на точку x=0.01. В отрезке [0; 0.01] умещается ровно один период синего графика и четыре периода графика зеленого. Иными словами за 0.01 секунды две волны успевают совершить 1 и 4 колебания соответственно. Нехитрая математика подскажет, нам, что это равно 1/0.01=100 и 4/0.01 = 400 колебаниям в секунду соответственно.

И вот теперь мы знаем, как эти две синусоиды выглядят формульно:

y_{green}=sin(2 \pi 400x)y_{blue}=0.75sin(2 \pi 100x)

Не забываем, что здесь делает 2\pi — это период колебания, и он дает числам 100 и 400 возможность трактоваться как "количество колебаний в секунду". Или если проще, то это частота колебания волны в герцах.

Вот и получаем, что зеленая волна — это звук в 400Hz на громкости 100%, а синяя волна — звук в 100Hz на громкости 75%.

Герцы — это хорошо, но может мы могли бы сказать, что это за ноты? И вот тут от теории звука мы плавно вступаем на территорию теории музыки.

Физика (и математика) музыки

Общеизвестно, что нот всего семь. Более точным же утверждением, правда, будет, что:

  • Нот всего семь

  • Но это касается западной музыки

  • Нот не совсем семь, а скорее двенадцать

  • Никто вам не запретит выходить за пределы этих двенадцати нот, и это делается повсеместно

А что это за такие 12 нот, и почему все таки не семь, ведь до ре ми фа соль ля си же? А вот смотрите:

Не спрашивайте, почему так, и какая за этим стоит логика. Скорее всего так исторически сложилось. Стоит заметить, что вообще нот бесконечное множество: после того как вы на пианино сыграли эти 12 нот, вы можете пойти дальше и сыграть снова эти же 12 нот, но уже в другой октаве. Это работает в обе стороны — как влево, к более басовым, низким нотам; так и вправо, к более высоким.

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

Почему именно такие значения закрепились как именные ноты, на которых строится вся западная музыка — это уже больше вопрос человеческого восприятия и культуры, которая восходит своими корнями к древней Греции, и мы, пожалуй, углубляться в эту тему не станем. Скажем лишь, что за эталонную частоту обычно берется ля четвертой октавы (A4), ей присвоили частоту 440Hz, а остальные ноты строятся от нее основываясь на специальных пропорциях, благозвучных для уха.

А вот синусоидальная волна, которую мы успели сгенерировать в нашем проекте — это Ми четвертой октавы с частотой 329.63Hz.


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

function sineWaveAt(sampleNumber, tone) {
	const time = sampleNumber / context.sampleRate
	return Math.sin(2 * Math.PI * tone * time)
}

Последняя строчка теперь нам плюс-минус понятна, поскольку:

ожидание y=a*sin(2\pi h x)
реальность Math.sin(2 * Math.PI * tone * time)

tone - в данном случае частота в герцах, time — это наш x, ведь не забываем, что ось x отвечает за время. Но как мы сформировали переменную time, и откуда взялись sampleNumber и sampleRate? И вот тут мы снова должны кратко нырнуть в теорию — в теорию оцифровки звука.

Оцифровка звука

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

Наверняка вы видели такие цифры, как 44.1kHz или 48kHz, когда речь заходила о CD или DVD качестве звука. Это показатели (кстати говоря, снова частоты, но уже другие) того, на сколько точек будет разделена одна секунда сигнала. Например, частота дискретизации в 44.1kHz — это раздербанивание одной секунды сигнала на целых 44100 точек. Соответственно, точка №44100 будет представлять звук на момент времени 1с, №66150 — 1.5с, №88200 — 2с и т.д.


Снова возвращаемся к коду!

function sineWaveAt(sampleNumber, tone) {
	const time = sampleNumber / context.sampleRate
	return Math.sin(2 * Math.PI * tone * time)
}

Как мы посчитали время? У Web Audio API есть свой аудио-контекст, который поддерживает какую-то конкретную частоту дискретизации звука, лежащую в переменной sampleRate. Собственно, чтобы выйти на время x, мы должны знать номер сэмпла, для которого мы хотим взять значение нашей синусоиды: sampleNumber. Ну и, наконец, деление номера семпла на частоту дискретизации дает нам точное время в секундах.

Наконец-таки мы смогли разобраться в двух-строчном коде! И все ради того, чтобы в следующий же момент я вам объявил, что у Web Audio API есть специальная oscillator-нода, которая сама умеет генерировать синусоидальный сигнал любой частоты, и мы могли не возиться с этими формулами вручную. Зато сколько опыта мы получили!

Осциллятор

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

Объекту OscillatorNode можно задать частоту в Hz, чтобы получить звук любой высоты. Но что интереснее, и с чем мы еще не сталкивались: сигналу можно задать не только форму синусоиды, но и так же форму квадрата, треугольника и форму пилы. У Википедии есть показательное изображение:

Приведу небольшой отрывок, чтобы вы могли сравнить их на слух. Последовательно будут воспроизведены синусоида, треугольник, квадрат и пила:

Те, кто играл в 8-битные приставки, могут словить легкое чувство дежавю — примерно так разработчики Денди могли менять тембр для игровых мелодий, варьируя звуки от очень мягких, плавных до резких и режущих ухо.

Теперь давайте создадим нашу осцилляторную ноду и сравним ее использование с тем, что у нас было написано в функции sineWaveAt:

const oscillator = context.createOscillator();
oscillator.frequency.value = toneInHz

В самом минималистичном варианте как бы и все: создали ноду, задали ей высоту звучания — готово! Но сам факт создания ноды и ее настройка не дают нам тут же звук в браузере. Поскольку, чтобы наш осциллятор был услышан, его сигнал нужно куда-то направить. И вот здесь мы постепенно начнем углубляться в Web Audio API, чего я не сделал для предыдущего примера, поскольку код с sineWaveAt в любом случае был на выброс.

Как я уже вскользь упоминал, Web Audio API предоставляет нам некий контекст, AudioContext. По сути своей он является глобальным объектом, у которого есть все необходимые методы для создания нод и их соединения друг с другом. Ноды в свою очередь могут быть:

  • Источниками звука, как наша OscillatorNode. Или, к примеру, существует нода, которая может воспроизвести mp3-файл

  • Преобразователями звука. Это ноды для различного эквалайзинга, наложения эха, изменения громкости и т.п.

  • Приемниками звука. На самом деле такая нода есть только одна, и она — что-то вроде виртуального динамика, на которую подается итоговый звук, и именно он будет слышен через ваши колонки или наушники

Основная суть и принцип работы Web Audio API: вы создаете необходимые вам ноды, соединяете их друг с другом, чтобы звук в нужном порядке проходил и преобразовывался через них, а в самом конце итоговый сигнал нужно подать на финальную ноду AudioContext.destination.

В нашем случае схема будет достаточно простой: у нас есть нода-осциллятор, и мы соединяем ее с AudioContext.destination, и в браузере будет слышен звук. Но я бы на вашем месте остерегался его слушать, поскольку здесь закрался нюанс. Громкость осциллятора очень высокая, потому что выдаваемый им сигнал лежит в знакомом нам диапазоне y\in[-1; 1], который означает что-то вроде "полновесной громкости".

Громкость сигнала нужно значительно убавить, и сама нода осциллятора не дает никаких настроек для этого. Потому что за громкость отвечает отдельная нода GainNode. У нее есть буквально одна настройка — gain. Это громкость в диапазоне [0; 1]. Я остановился на 0.25.

Итого, схематично мы должны получить какой-то такой результат:

Выразим это в коде:

context = new AudioContext();

const oscillator = context.createOscillator()
oscillator.frequency.value = tone

const gain = context.createGain()
gain.gain.value = 0.25

oscillator.connect(gain)
gain.connect(context.destination)

oscillator.start(context.currentTime)

Создали контекст, создали и настроили две ноды, все подсоединили и в конце запустили наш осциллятор командой start. Только после этого пинка осциллятор станет генерировать звук, который пойдет по всему нашему незамысловатому контуру: Gain-нода примет звук с осциллятора, ослабит его на четверть и передаст результат на выход.

Результат будет буквально таким же, как и в прошлый раз. Поэтому давайте ради разнообразия зададим осциллятору треугольную форму:

oscillator.type = "triangle";

Теперь это звучит вот так:

Воспроизводим мелодию

Теперь полученный нами сигнал будем использовать для того, чтобы воспроизвести какую-нибудь простую мелодию. Я для этой цели выбрал начальный отрывок из песни Seven Nation Army. Полагаю, большинство может воспроизвести эти ноты в своей голове, поэтому я не стану приводить отрывок из этой песни.

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

  • Как долго проигрывать каждый звук?

  • В какой единице измерения задавать длительность нот?

  • Как задать темп песни? И если захочется ускорить или замедлить общий темп, как сохранить корректную длительность нот относительно друг друга?

  • Как резать песню на такты, и как количество тактов зависит от темпа песни?

Вопросов много, поэтому разбираться будем постепенно. Начнем с того, как решаются такие проблемы у настоящих музыкантов. Решаются они нотной записью — мелодия записывается на нотный стан, в котором есть вся необходимая информация, связанная с временными характеристиками песни. Наш отрывочек на нотном стане мог бы выглядеть вот так:

Видите семь последовательно записанных нот? В качестве любопытного упражнения попробуйте воспроизвести в голове Seven Nation Army и проследовать за каждой нотой на нотном стане. Ставь класс, если вышло. А теперь разберемся, как, что и зачем здесь записано.

Начнем с любопытной записи 4/4 после скрипичного ключа. Она означает, что под эту песню вы можете топать ногами, хлопать руками и циклично кричать "раз, два, три, четыре! раз, два, три, четыре!". И если вы поймаете грув и будете делать это в нужном темпе, то вам будет очень весело.

♩=110 наверху как раз обозначает темп песни, который подскажет с какой скорость нужно хлопать и кричать. Темп измеряется в bpm или beats per minute (удары в минуту). Дословно 110 bpm нужно трактовать так: если непрерывно играть ноту длительностью ♩, то за одну минуту их будет проиграно ровно 110 штук.

Но кто такой этот ваш ♩?! Здесь проще будет сначала показать, как все типы нот умещаются в такт 4/4:

Итак, разные ноты по-разному помещаются в такт "один, два, три, четыре!". В первом такте мы видим целую ноту — она звучит весь такт, все 4 притопа. Во втором такте размещены половинные ноты, их уже две — одна звучит пока "раз, два", вторая звучит пока "три, четыре!". В последующих тактах идут четвертные, восьмые и шестнадцатые ноты. Полагаю, принцип вы поняли.

Интересующая нас ♩ — четвертная нота. Она интересна тем, что в такте размерностью 4/4 она как раз играется ровно 4 раза и символизирует каждый "раз", "два", "три" и "четыре". Темп 110 bpm как раз завязан на количество таких нот в минуту. Причем никто не предлагает высчитывать все эти длительности вручную и считать до 110. Обычно музыканты и так хорошо знают, что песня с более-менее обычным, умеренным темпом — это 110-125 bpm; 90 bpm — это что-то медленное; 70 bmp — тягуче медленное; 150 bpm — это что-то быстрое, задорное; а 200 bpm — экстрим. Ну и всегда есть камертон, которому можно задать нужный bmp, и он сам будет отщелкивать четвертные ноты, чтобы вы могли играть в стабильном темпе даже без барабанщика.

Внимание: не запутайтесь в флажках! Запись ♫ — это всего лишь две сгруппированные ноты ♪♪. Аналогично ♬=

Источник: https://habr.com/ru/articles/831852/


Интересные статьи

Интересные статьи

Зачем создавать паровой мотоцикл?Весь мир сейчас пытается продвигать технику в будущее. Ищут всё более экономичные, мощные и удобные двигатели. Мне же наоборот, захотелось заглянуть в прошлое и оживит...
Технология RFID-имплантов существует довольно давно. RFID (Radio Frequency Identification) — это технология идентификации с использованием радиочастот. Главное её преимущество — это отсутствие необход...
В одной из своих статей я уже описывал как можно реализовать рефлексию при помощи source generator-ов. Тогда цель была продемонстрировать что такое эти ваши генераторы, а сама рефлексия была лишь прим...
Разработка игр может быть очень увлекательной, особенно в самом начале, когда у тебя много идей и только от твоей фантазии зависит, в какую сторо...
В апреле 2020-го года разработчиками платформы .NET 5 был анонсирован новый способ генерации исходного кода на языке программирования C# — с помощью реализации интерфейса ISourceGener...