Делаем браузерный midi-конвертор/генератор музыки для воспроизведения на шаговых двигателях

от автора

                                                                                                                                                                                                                          Chidodj

Сегодня мы займёмся одной интересной затеей, которая пришла мне в голову, уже достаточно давно, когда я впервые увидел, как воспроизводят музыку на двигателях, в частности, играют Имперский марш из Звёздных войн, на приводах 3,5-дюймовых дискет, и не только, посылая с помощью микроконтроллера, высокочастотные сигналы на двигатель, издающий при этом звук.

Только, обычно, этот звук двигателей является отрицательным явлением, благодаря чему пользователям даже приходится устройство с этими двигателями (например, ЧПУ-станок или 3D принтер), ставить в другую комнату, чтобы они не докучали.

Мы же заставим этот звук служить нашим интересам, ублажая наши чресла наш слух. 😀

Посему: а сделаем ка, универсальный конвертер/генератор музыки, для игры на двигателях! Никто ведь не против? Нет? Ок, тогда поехали…:-D

Что, как и зачем

Немного продолжая предысторию, можно тут ещё сказать, что подобный подход — воспроизведение «специального», не сугубо технического звука, двигателями, довольно распространён: в частности, подобным образом, дроны бытового назначения, обычно информируют своего пользователя, о разных событиях: включение, переход из одного пункта меню в другое (на пульте управления, во время настроек) и т.д.

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

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

Среди них, я бы выделил два наиболее очевидных варианта:

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

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

  2. воспроизведение музыки, в формате midi: для микроконтроллеров с малым объёмом памяти это способ видится одним из самых интересных: в его рамках, надо оперировать только ограниченным числом нот и последовательностью их воспроизведения, варьируя, кроме этого, продолжительностью их звучания, и промежутками между ними.

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

Как показали дальнейшие эксперименты, этот выбранный путь (в рамках обозначенных целей — воспроизведения простых звуков), оказался более чем верным: я пытался загружать разные треки в микроконтроллер, однако, несмотря на то, что некоторые треки были длиной в минуту и более, — они у меня никогда не занимали где-то более 15-18% памяти микроконтроллера! Неплохой результат! 🙂

Кстати о микроконтроллере: в качестве объекта для загрузки, я выбрал микроконтроллер esp32 wroom32, которому был подключен драйвер двигателя, где, уже к нему, был подключен шаговый двигатель типоразмера Nema 17.

То есть, другими словами, я решил играть музыку на шаговом двигателе — продолжить, так сказать, «традиции олдов» 🙂 

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

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

Кстати, тут надо ещё отметить такой момент, а зачем это вообще может понадобиться кому-то (играть музыку с помощью двигателя)?

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

Например, как вам понравится такое: загрузить трек из какой-нибудь компьютерной игры, который будет воспроизводиться на двигателе ЧПУ станка или самодельного робота, после завершения работы — какой-нибудь «mission complete» или «stage clear»-саундтрек, из компьютерной игры. 

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

Или даже проигрывание грустной музыки, в стиле «game over» — если что-то пошло не так! 😀 

Так что тут возникает много интересных возможностей — ограниченных только вашей фантазией…

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

Поэтому, ранее, подобным баловались только senior-ы embedded-направления. 

Но, всё меняется — теперь, вы можете тоже попробовать;-)

Забегая вперёд, скажу, что, кажется, у меня получилось (но потребовало просто какого-то безумного количества итераций – более 100!) :-))

Но, сначала немного дополнительной полезной информации…

Краткая теория воспроизведения звука на двигателях

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

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

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

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

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

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

Кроме того, можно играть громкостью звука, чисто программным способом, используя частоту шагов, форму питающего тока (шаг, микрошаг) — сразу скажу, что эта часть (программное усиление громкости звука) у меня не особо оптимизирована, так что тут есть некоторые возможности для улучшений;-)

Что такое MIDI, совсем кратко

Musical Instrument Digital Interface (MIDI) предназначен для кодирования в цифровом формате звуковых событий, а не непосредственно самого звука — и этим он отличается от собственно звуковых форматов (mp3, wav и т.д.), кодируя звук в виде нот, их длительности, громкости, и инструментов, на которых они должны быть воспроизведены, — то есть, говоря другими словами, в рамках этого интерфейса оперируют инструкциями для создания музыки, а не непосредственно самой звуковой информацией. 

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

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

  • управляющими (смена громкости, включение/отключение эффектов и т.д.);

  • нотными (начало и конец воспроизведения ноты);

  • мета-событиями (как пример — изменение темпа воспроизведения).

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

Я здесь рассказал совсем кратко, применительно к конвертеру, о котором пойдёт речь ниже, вы же, при желании более глубоко ознакомиться с форматом midi, можете пройти вот по этой ссылке.

Браузерный обработчик-конвертер midi- звукового файла в программу под Arduino IDE, для загрузки в esp32 wroom32

Итак, что получилось в итоге?

Принцип действия

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

Далее, код анализирует файл, с целью определить, на какой дорожке, какие инструменты расположены. 

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

Например: для бас-гитары (низкие ноты), скрипки (высокие ноты), пианино (широкий диапазон — в зависимости от того, где расположена клавиша, может играть как высокие, так и низкие ноты). 

Кроме того, дополнительно анализируются косвенные признаки, например, если можно так сказать, «рисунок воспроизведения» — насколько часто и как ритмично повторяются ноты (так, к примеру, можно выявить барабаны), насколько продолжительно и в какой последовательности (например, быстрое воспроизведение, перебором нот по очереди — выявляет арфу).

Но, сразу нужно сказать, что всё описанное выше определение предположительное, и довольно примерное, поэтому, нельзя его назвать на 100% верным, и это делается просто «для справки пользователя» — чтобы хоть примерно понимать, какие дорожки, с какими инструментами стоит оставить звучащими, а какие стоит выключить (зачем это делать — об этом ещё будет ниже).

Далее, происходит фильтрация нот, согласно настройкам пользователя: включение/отключение фильтрации ударных, фильтрация за пределами частотного диапазона (можем заставить исполняться мелодию только в более басовом/среднем/высоком звучании), и за пределами границ временного диапазона (проще говоря — можно вырезать требуемый кусок, для загрузки в микроконтроллер; это полезная «фишка» , так как зачастую, требуется маленький, особо приглянувшейся фрагмент, а не вся мелодия). 

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

В ходе генерации, в частности, создаётся три массива (PROGMEM), для хранения частот нот, их продолжительности и пауз между ними.

В ходе экспериментов, для проигрывания музыки на шаговом двигателе, применялся самый простой драйвер двигателя (HG7881CP), так называемый «Н-мост» , которому были подключены четыре вывода шагового двигателя:

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

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

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

Интерфейс

Интерфейс программы выглядит следующим образом.

Вот так, до загрузки midi-трека:

А вот так — после загрузки трека (картинка ниже). Как мы видим, появилась некоторая информация о треке, в самом верху интерфейса, а также, ниже надписи «Фильтровать ударные (канал 10)» отобразились дорожки, которые были в этом треке. Слева от каждой дорожки есть галочка, нажимая/отжимая которую можно включать/отключать конкретную дорожку. Кроме того, в самом низу появился сгенерированный код для микроконтроллера, который можно скачать (нажав зелёную кнопку «Скачать код») или скопировать, нажав синюю кнопку «Копировать», в правом верхнем углу окна с кодом:

Включение/отключение дорожек

Как можно видеть, в верхней части программы находится общая краткая теория, чтобы, для тех, кто в первый раз сталкивается с midi-форматом, было несколько более понятно происходящее.

Но, ещё раз проговорю словами, чтобы стало ещё понятнее: как мы уже рассматривали выше, в рамках формата midi, мы имеем ряд дорожек, на которых располагаются разные инструменты, и, на голубой вкладке как раз и показано, как, в теории, они должны располагаться (на каких каналах). 

Однако, теория теорией, но, практическая жизнь вносит свои коррективы: та последовательность, как должны быть расположены инструменты, и как это показано на голубой вкладке — вовсе не факт, что так будет повторяться в реальной жизни! 🙂 

Разработчик конкретного трека, может располагать их, как ему взбредёт в голову (и это вы ещё увидите, в результате своих собственных экспериментов) 🙂

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

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

Тем не менее, многие следуют стандартам и поэтому их треки имеют стандартную структуру, распознающуюся большинством устройств. 

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

Каким образом мы узнаём, на каком канале что в реальности расположено: в этот браузерный конвертер встроен автоматический анализатор, который при загрузке midi-трека, производит анализ дорожек, которые содержит этот трек, и, предположительно, отображает инструменты, обнаруженные в реальности.

Тут ещё нужна небольшая справка: а зачем вообще я сделал так, чтобы дорожки отображались в интерфейсе?

Не из-за праздного же интереса, чтобы просто узнать, как устроен конкретный midi-файл?

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

Поэтому, требуется опытным путём включать/отключать определенные дорожки, подобрав такое их сочетание, которое будет звучать узнаваемо (если вы хотите, воспроизвести какую-то известную мелодию, и, нужно, чтобы люди её узнали). 

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

То есть, нужно экспериментировать, а эта возможность с включением/отключением дорожек (нажимая на галочки слева от них) — просто одна из таких возможностей, для варьирования и экспериментов…

Урезание/расширение диапазона звучащих частот
(ползунки «Максимальная частота», «Минимальная частота» )

Вариант упрощения/усложнения трека, который может быть осуществлён, с применением способа выше, — включением/отключением отдельных каналов, можно рассматривать как более грубый вариант настройки. 

Если нужен более тонкий вариант, это можно осуществить ещё дополнительно с помощью, урезания/расширения диапазона частот — используя соответствующие ползунки (максимальная частота/минимальная частота). Эти изменения отображаются в коде, предназначенном для загрузки в микроконтроллер.

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

Темп

С помощью ползунка «темп» можно настраивать скорость воспроизведения трека: уменьшив его скорость до 50% от текущего, или увеличив на 200% от текущего. Эти изменения отображаются в коде, предназначенном для загрузки в микроконтроллер.

Громкость

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

Фильтрация ударных

Почти в самом низу интерфейса, над окном с кодом, можно видеть галочку «Автоматически фильтровать ударные (канал 10)» — если её поставить, и если в конкретном midi-треке, есть ударные (ожидается, что они будут на 10 канале — но это вовсе не факт, как мы знаем :-D), то они будут отфильтрованы (убраны) — к слову, в аналогичных же целях, как можно видеть, в частотной фильтрации — минимальная частота установлена по умолчанию на 100 герц. 

Это также сделано с целью отфильтровать ударные, если они будут расположены на ином канале (не на десятом). 

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

А иной раз, наоборот, очень хорошо оставить ударные — басовито так «прёт», аж двигатель со стола норовит спрыгнуть…:-)))

Эти изменения отображаются в коде, предназначенном для загрузки в микроконтроллер.

В общем — жмите, отжимайте галочку, пробуйте, экспериментируйте и да пребудет с вами сила…

ВАЖНО — РЕШЕНИЕ ПРОБЛЕМ: как я и говорил выше, в реальных midi‑треках творится всё, что угодно (да вы и сами это увидите), а парсер не умеет понимать «все ситуации на свете».

Поэтому: если вы нажали на «Воспроизвести» в предпрослушивании в браузере, секундомер пошёл, а звука нет — «поздравляю!» (в кавычках) — вам попался проблемный трек!

Что делать: нажать на кнопку «стоп», перемотать начало трека, с помощью ползунка «Начало фрагмента» и нажать кнопку «Воспроизвести». Проделать так, на разных участках — иногда бывает, что вначале трека идёт огромный кусок тишины.

С чем ещё сталкивался: несмотря на то, что пле ер предпрослушивания в браузере подглючивает в виду описанных выше причин, я заметил, что распознавание трека и генерация кода для Arduino IDE всё равно работает корректно (возможно, что я не сталкивался с другими ситуациями просто — не исключаю).

Если вообще ничего не получается с этим треком: бросаем йего и вытираем скупую слезу — селяви 🙂

Однако в реальности всё далеко не так плохо и многое работает вообще без каких то проблем или с минимальными проблемами (перемотать начало чуть подальше).

Ну и самое главное — код (копируем, вставляем в блокнот, сохраняем c расширением .html и запускаем двойным кликом):

Код конвертора / генератора
<!--   MIDI to Arduino/esp32 controlled Stepper Motor Music Converter   Created with assistance from DeepSeek Chat AI   https://deepseek.com -->  <!DOCTYPE html> <html> <head>   <title>Конвертер midi-файлов - в музыку для шагового двигателя</title>   <style>     body {       font-family: Arial, sans-serif;       max-width: 800px;       margin: 0 auto;       padding: 20px;       line-height: 1.6;     }     .panel {       background: #f5f5f5;       padding: 20px;       border-radius: 8px;       margin-bottom: 20px;     }     button {       padding: 10px 15px;       background: #4CAF50;       color: white;       border: none;       cursor: pointer;       margin: 5px;     }     button:disabled {       background: #cccccc;     }     #midiInfo {       margin: 15px 0;       min-height: 60px;     }     #codeOutput {       width: 100%;       height: 300px;       font-family: monospace;       position: relative;     }     .status {       padding: 10px;       margin: 10px 0;       border-radius: 4px;     }     .status-info {       background: #d4edda;       color: #155724;     }     .status-error {       background: #f8d7da;       color: #721c24;     }     .slider-container {       margin: 10px 0;     }     .slider-label {       display: flex;       justify-content: space-between;       margin-bottom: 5px;     }     .copy-btn {       position: absolute;       top: 5px;       right: 5px;       padding: 5px 10px;       background: #2196F3;       color: white;       border: none;       border-radius: 3px;       cursor: pointer;     }     .track-list {       margin: 15px 0;       padding: 10px;       background: #fff;       border-radius: 5px;     }     .track-item {       margin: 8px 0;       padding: 8px;       background: #f9f9f9;       border-radius: 3px;       display: flex;       align-items: center;     }     .track-item input {       margin-right: 10px;     }     .warning {       background: #fff3cd;       color: #856404;       padding: 15px;       border-radius: 5px;       margin: 15px 0;       border-left: 4px solid #ffc107;     }     .theory {       background: #e7f5fe;       padding: 15px;       border-radius: 5px;       margin: 15px 0;       font-size: 0.9em;       border-left: 4px solid #2196F3;     }     .instrument-icon {       margin-right: 8px;       font-size: 1.2em;     }     .track-details {       font-size: 0.85em;       color: #666;       margin-left: 5px;     }     .drums-label {       color: #d32f2f;       font-weight: bold;       margin-left: 5px;     }     .audio-controls {       margin: 15px 0;       display: flex;       gap: 10px;       align-items: center;     }     .trim-controls {       margin: 15px 0;       display: grid;       grid-template-columns: 1fr 1fr;       gap: 15px;     }     .trim-slider {       display: flex;       flex-direction: column;       gap: 5px;     }     .time-display {       font-family: monospace;       margin-left: auto;     }   </style> </head> <body>   <h2>Конвертер midi-файлов - в музыку для шагового двигателя</h2>      <div class="panel">     <input type="file" id="midiUpload" accept=".mid,.midi" />     <div id="midiInfo">Загрузите MIDI-файл</div>          <div class="warning">       <strong>⚠ Важно!</strong> Некоторые MIDI-файлы могут содержать:       <ul>         <li>Нестандартные форматы дорожек</li>         <li>События без нотных сообщений</li>         <li>Альтернативные способы кодирования темпа</li>       </ul>     </div>      <div class="theory">       <strong>🎼 Стандартное расположение инструментов (General MIDI):</strong>       <ul>         <li><strong>Канал 1:</strong> 🎹 Фортепиано (основная мелодия)</li>         <li><strong>Канал 2-5:</strong> 🎻 Струнные / 🎷 Духовые</li>         <li><strong>Канал 6:</strong> 🎸 Бас-гитара</li>         <li><strong>Канал 7-8:</strong> 🎶 Аккомпанемент</li>         <li><strong>Канал 10:</strong> 🥁 Ударные (стандарт)</li>         <li><strong>Канал 11-16:</strong> 🎺 Солирующие инструменты</li>       </ul>     </div>      <div class="audio-controls">       <button id="playBtn">▶️ Воспроизвести</button>       <button id="stopBtn" disabled>⏹️ Стоп</button>       <div class="time-display" id="timeDisplay">0:00 / 0:00</div>     </div>      <div class="trim-controls">       <div class="trim-slider">         <label for="trimStart">Начало фрагмента: <span id="trimStartValue">0:00</span></label>         <input type="range" id="trimStart" min="0" max="100" value="0">       </div>       <div class="trim-slider">         <label for="trimEnd">Конец фрагмента: <span id="trimEndValue">0:00</span></label>         <input type="range" id="trimEnd" min="0" max="100" value="100">       </div>     </div>     <div id="trimInfo" style="text-align: center; margin: 10px 0;">Фрагмент: 0:00 - 0:00</div>      <div class="slider-container">       <div class="slider-label">         <span>Максимальная частота: <span id="maxFreqValue">2000</span> Гц</span>       </div>       <input type="range" id="maxFreq" min="100" max="5000" value="2000" step="10">     </div>          <div class="slider-container">       <div class="slider-label">         <span>Минимальная частота: <span id="minFreqValue">100</span> Гц</span>       </div>       <input type="range" id="minFreq" min="50" max="2000" value="100" step="10">     </div>          <div class="slider-container">       <div class="slider-label">         <span>Темп: <span id="tempoValue">100</span>%</span>       </div>       <input type="range" id="tempo" min="50" max="200" value="100" step="1">     </div>          <div class="slider-container">       <div class="slider-label">         <span>Громкость: <span id="volumeValue">100</span>%</span>       </div>       <input type="range" id="volume" min="0" max="100" value="100" step="1">     </div>          <div>       <input type="checkbox" id="filterDrums" checked>       <label for="filterDrums">Фильтровать ударные (канал 10)</label>     </div>          <div id="trackSelection" class="track-list" style="display: none;">       <h4>Дорожки в файле:</h4>     </div>          <button id="downloadBtn" disabled>Скачать код</button>     <div id="status" class="status status-info">Кликните по странице для активации звука</div>   </div>    <div style="position: relative;">     <textarea id="codeOutput" readonly></textarea>     <button id="copyBtn" class="copy-btn" disabled>Копировать</button>   </div>    <script>     const elements = {       midiUpload: document.getElementById('midiUpload'),       midiInfo: document.getElementById('midiInfo'),       downloadBtn: document.getElementById('downloadBtn'),       copyBtn: document.getElementById('copyBtn'),       status: document.getElementById('status'),       codeOutput: document.getElementById('codeOutput'),       filterDrums: document.getElementById('filterDrums'),       minFreq: document.getElementById('minFreq'),       maxFreq: document.getElementById('maxFreq'),       tempo: document.getElementById('tempo'),       volume: document.getElementById('volume'),       minFreqValue: document.getElementById('minFreqValue'),       maxFreqValue: document.getElementById('maxFreqValue'),       tempoValue: document.getElementById('tempoValue'),       volumeValue: document.getElementById('volumeValue'),       trackSelection: document.getElementById('trackSelection'),       playBtn: document.getElementById('playBtn'),       stopBtn: document.getElementById('stopBtn'),       timeDisplay: document.getElementById('timeDisplay'),       trimStart: document.getElementById('trimStart'),       trimEnd: document.getElementById('trimEnd'),       trimStartValue: document.getElementById('trimStartValue'),       trimEndValue: document.getElementById('trimEndValue'),       trimInfo: document.getElementById('trimInfo')     };      let audioContext = null;     let currentMidiData = null;     let isPlaying = false;     let playbackStartTime = 0;     let totalDuration = 0;     let trimStart = 0;     let trimEnd = 100;     let bpm = 120;     let activeOscillators = new Set();     let selectedStartTime = 0;     let selectedEndTime = 0;      function initAudio() {       if (!audioContext) {         audioContext = new (window.AudioContext || window.webkitAudioContext)();         showStatus("Аудио активировано. Теперь можно загружать MIDI");       }     }      function updateSliderValues() {       elements.minFreqValue.textContent = elements.minFreq.value;       elements.maxFreqValue.textContent = elements.maxFreq.value;       elements.tempoValue.textContent = elements.tempo.value;       elements.volumeValue.textContent = elements.volume.value;              selectedStartTime = totalDuration * (parseInt(elements.trimStart.value) / 100);       selectedEndTime = totalDuration * (parseInt(elements.trimEnd.value) / 100);              elements.trimStartValue.textContent = formatTime(selectedStartTime);       elements.trimEndValue.textContent = formatTime(selectedEndTime);              trimStart = parseInt(elements.trimStart.value);       trimEnd = parseInt(elements.trimEnd.value);       elements.trimInfo.textContent = `Фрагмент: ${formatTime(selectedStartTime)} - ${formatTime(selectedEndTime)}`;              // Обновляем таймер сразу при изменении ползунков       if (!isPlaying) {         elements.timeDisplay.textContent = `${formatTime(selectedStartTime)} / ${formatTime(selectedEndTime)}`;       }     }      function formatTime(seconds) {       const mins = Math.floor(seconds / 60);       const secs = Math.floor(seconds % 60);       return `${mins}:${secs < 10 ? '0' : ''}${secs}`;     }      function playMidi() {       if (!currentMidiData || isPlaying || !audioContext) return;              if (audioContext.state === 'suspended') {         audioContext.resume().then(() => {           startPlayback();         });       } else {         startPlayback();       }     }      function startPlayback() {       stopPlayback();              isPlaying = true;       elements.playBtn.disabled = true;       elements.stopBtn.disabled = false;              const startTime = audioContext.currentTime;       playbackStartTime = startTime;              const startTimePercent = trimStart / 100;       const endTimePercent = trimEnd / 100;       const fragmentDuration = selectedEndTime - selectedStartTime;              elements.timeDisplay.textContent = `${formatTime(selectedStartTime)} / ${formatTime(selectedEndTime)}`;              const selectedTracks = getSelectedTracks();       const tempoFactor = 100 / elements.tempo.value;       const volume = elements.volume.value / 100;              const microsecondsPerTick = 60000000 / (bpm * currentMidiData.header.ticksPerBeat);              selectedTracks.forEach(trackIndex => {         const track = currentMidiData.tracks[trackIndex];                  track.notes.forEach(note => {           if (elements.filterDrums.checked && note.channel === 9) return;                      const noteTime = note.time * microsecondsPerTick / 1000000;                      if (noteTime >= selectedStartTime && noteTime <= selectedEndTime) {             const adjustedTime = (noteTime - selectedStartTime) * tempoFactor;             const noteDuration = (currentMidiData.header.ticksPerBeat / 2) * microsecondsPerTick / 1000000 * tempoFactor;                          try {               const oscillator = audioContext.createOscillator();               const gainNode = audioContext.createGain();                              oscillator.type = 'sine';               oscillator.frequency.value = midiToFrequency(note.midi);               gainNode.gain.value = (note.velocity / 127) * volume;                              oscillator.connect(gainNode);               gainNode.connect(audioContext.destination);                              oscillator.start(startTime + adjustedTime);               oscillator.stop(startTime + adjustedTime + noteDuration);                              activeOscillators.add(oscillator);               oscillator.onended = () => activeOscillators.delete(oscillator);             } catch (e) {               console.error("Ошибка создания осциллятора:", e);             }           }         });       });              const updateTime = () => {         if (!isPlaying || !audioContext) return;                  const currentPlayTime = audioContext.currentTime - playbackStartTime;         const currentTrackTime = selectedStartTime + currentPlayTime;                  if (currentTrackTime <= selectedEndTime) {           elements.timeDisplay.textContent = `${formatTime(currentTrackTime)} / ${formatTime(selectedEndTime)}`;           requestAnimationFrame(updateTime);         } else {           stopPlayback();         }       };              requestAnimationFrame(updateTime);     }      function stopPlayback() {       isPlaying = false;       elements.playBtn.disabled = false;       elements.stopBtn.disabled = true;              activeOscillators.forEach(osc => {         try {            osc.stop();           osc.disconnect();         } catch (e) {           console.error("Ошибка остановки осциллятора:", e);         }       });       activeOscillators.clear();              // Возвращаем таймер к началу выбранного фрагмента       elements.timeDisplay.textContent = `${formatTime(selectedStartTime)} / ${formatTime(selectedEndTime)}`;     }      function getSelectedTracks() {       const selectedTracks = [];       const checkboxes = elements.trackSelection.querySelectorAll('input[type="checkbox"]');              checkboxes.forEach((cb, index) => {         if (cb.checked) selectedTracks.push(index);       });              return selectedTracks;     }      function analyzeTrack(track, index) {       if (!track.notes || track.notes.length === 0) {         return {           instrument: "❓ Пустая дорожка",           details: "Нет нот для анализа",           isDrums: false,           channel: null         };       }        const channel = track.notes[0].channel + 1;       const notes = track.notes.map(n => n.midi);       const minNote = Math.min(...notes);       const maxNote = Math.max(...notes);       const noteRange = maxNote - minNote;       const isDrums = (track.notes[0].channel === 9);              let instrument = "🎵 Инструмент";       let details = `Канал ${channel}, ноты: ${minNote}-${maxNote}`;        if (isDrums) {         instrument = "🥁 Ударные";       } else if (minNote < 40 && noteRange < 12) {         instrument = "🎸 Бас";       } else if (minNote > 60 && noteRange > 12) {         instrument = "🎹 Мелодия";       } else if (noteRange > 24) {         instrument = "🎼 Аккомпанемент";       } else if (channel === 10) {         instrument = "🥁 Ударные (канал 10)";       }        details = `Нот: ${track.notes.length}, ${details}`;        return {         instrument,         details,         isDrums,         channel       };     }      function createTrackSelection(tracks) {       elements.trackSelection.innerHTML = '<h4>Дорожки в файле:</h4>';              tracks.forEach((track, index) => {         const analysis = analyzeTrack(track, index);         const div = document.createElement('div');         div.className = 'track-item';                  div.innerHTML = `           <input type="checkbox" id="track-${index}" checked>           <label for="track-${index}">             <span class="instrument-icon">${analysis.instrument.split(' ')[0]}</span>             <strong>Дорожка ${index + 1}:</strong> ${analysis.instrument}             <span class="track-details">${analysis.details}</span>             ${analysis.isDrums ? '<span class="drums-label">[ударные]</span>' : ''}           </label>         `;                  div.querySelector('input').addEventListener('change', handleChanges);         elements.trackSelection.appendChild(div);       });              elements.trackSelection.style.display = 'block';     }      function parseMidi(arrayBuffer) {       const dataView = new DataView(arrayBuffer);       let pos = 0;              if (dataView.getUint32(pos) !== 0x4D546864) throw new Error("Неверный MIDI-файл");       pos += 8;              const format = dataView.getUint16(pos);       pos += 2;       const numTracks = dataView.getUint16(pos);       pos += 2;       const ticksPerBeat = dataView.getUint16(pos);       pos += 2;              const tracks = [];       let totalNotes = 0;       let maxTime = 0;       let tempo = 500000;       let hasNotes = false;              for (let i = 0; i < numTracks; i++) {         if (dataView.getUint32(pos) !== 0x4D54726B) throw new Error("Ошибка формата дорожки");         pos += 4;                  const trackLength = dataView.getUint32(pos);         pos += 4;         const trackEnd = pos + trackLength;                  const trackNotes = [];         let currentTime = 0;         let currentChannel = 0;         let runningStatus = null;                  while (pos < trackEnd) {           let deltaTime = 0;           let byte;           do {             byte = dataView.getUint8(pos++);             deltaTime = (deltaTime << 7) | (byte & 0x7F);           } while (byte & 0x80);                      currentTime += deltaTime;           let eventTypeByte = dataView.getUint8(pos);                      if ((eventTypeByte & 0x80) === 0) {             if (runningStatus === null) {               throw new Error("Неожиданный статус выполнения");             }             eventTypeByte = runningStatus;           } else {             runningStatus = eventTypeByte;             pos++;           }                      const eventType = eventTypeByte & 0xF0;                      if (eventType === 0x90) {             const note = dataView.getUint8(pos++);             const velocity = dataView.getUint8(pos++);             currentChannel = eventTypeByte & 0x0F;                          if (velocity > 0) {               trackNotes.push({                 midi: note,                 time: currentTime,                 channel: currentChannel,                 velocity: velocity               });               totalNotes++;               hasNotes = true;                              if (currentTime > maxTime) maxTime = currentTime;             }           } else if (eventType === 0x80) {             pos += 2;           } else if (eventTypeByte === 0xFF && dataView.getUint8(pos) === 0x51) {             pos++;             const length = dataView.getUint8(pos++);             tempo = 0;             for (let j = 0; j < length; j++) {               tempo = (tempo << 8) | dataView.getUint8(pos++);             }           } else {             const eventLength = getMidiEventLength(eventTypeByte, dataView, pos);             pos += eventLength;           }         }                  if (trackNotes.length > 0) {           tracks.push({ notes: trackNotes });         }       }              if (!hasNotes) {         throw new Error("Файл не содержит нотных событий");       }              bpm = Math.round(60000000 / tempo);              const microsecondsPerTick = tempo / ticksPerBeat;       totalDuration = maxTime * microsecondsPerTick / 1000000;       selectedStartTime = 0;       selectedEndTime = totalDuration;              return {          tracks,          header: { format, ticksPerBeat },          totalNotes,         duration: totalDuration       };     }          function getMidiEventLength(eventType, dataView, pos) {       const highNibble = eventType & 0xF0;       if (highNibble === 0x80 || highNibble === 0x90 || highNibble === 0xA0 ||            highNibble === 0xB0 || highNibble === 0xE0) return 2;       else if (highNibble === 0xC0 || highNibble === 0xD0) return 1;       else if (eventType === 0xFF) return 2 + dataView.getUint8(pos + 1);       return 0;     }      function midiToFrequency(note) {       const freq = 440 * Math.pow(2, (note - 69) / 12);       return isFinite(freq) ? freq : 440;     }      function handleChanges() {       updateSliderValues();       if (currentMidiData) {         processMidi(currentMidiData);       }     }      function processMidi(midiData) {       const filterDrums = elements.filterDrums.checked;       const minFreq = parseInt(elements.minFreq.value);       const maxFreq = parseInt(elements.maxFreq.value);       const tempoFactor = 100 / elements.tempo.value;              const selectedTracks = getSelectedTracks();              const startPercent = trimStart / 100;       const endPercent = trimEnd / 100;       const startTime = midiData.duration * startPercent;       const endTime = midiData.duration * endPercent;              const allNotes = [];       selectedTracks.forEach(trackIndex => {         const track = midiData.tracks[trackIndex];                  track.notes.forEach(note => {           if (filterDrums && note.channel === 9) return;                      const microsecondsPerTick = 60000000 / (bpm * midiData.header.ticksPerBeat);           const noteTime = note.time * microsecondsPerTick / 1000000;                      if (noteTime < startTime || noteTime > endTime) return;                      const freq = midiToFrequency(note.midi);           if (freq < minFreq || freq > maxFreq) return;                      const adjustedTime = (noteTime - startTime) * tempoFactor;           const noteDuration = 100 * tempoFactor;                      allNotes.push({             midi: note.midi,             time: adjustedTime,             duration: noteDuration,             velocity: note.velocity           });         });       });        allNotes.sort((a, b) => a.time - b.time);       const optimizedNotes = optimizeNotes(allNotes, minFreq, maxFreq);       generateArduinoCode(optimizedNotes);     }      function optimizeNotes(notes, minFreq, maxFreq) {       const optimized = [];       let lastEndTime = 0;              for (const note of notes) {         const freq = midiToFrequency(note.midi);         if (freq < minFreq || freq > maxFreq) continue;                  const startTime = Math.max(lastEndTime, note.time);         const endTime = startTime + note.duration;                  optimized.push({           freq: freq,           duration: note.duration,           startTime: startTime,           endTime: endTime         });                  lastEndTime = endTime;       }              return optimized;     }      function generateArduinoCode(notes) {       if (notes.length === 0) {           elements.codeOutput.value = "// Нет нот для воспроизведения";           elements.downloadBtn.disabled = true;           elements.copyBtn.disabled = true;           return;       }              const tempoFactor = elements.tempo.value / 100;              const freqs = notes.map(n => Math.round(n.freq)).filter(f => !isNaN(f));       const durations = notes.map(n => Math.round(n.duration / tempoFactor));              const delays = [];       for (let i = 1; i < notes.length; i++) {           delays.push(Math.max(1, Math.round((notes[i].startTime - notes[i-1].endTime) / tempoFactor)));       }       delays.push(50 / tempoFactor);              elements.codeOutput.value = `#include <Arduino.h>  const uint8_t COIL_A1 = 12; const uint8_t COIL_A2 = 14; const uint8_t COIL_B1 = 27; const uint8_t COIL_B2 = 26;  const uint16_t MIN_FREQ = ${elements.minFreq.value}; const uint16_t MAX_FREQ = ${elements.maxFreq.value};  const uint16_t melodyFreqs[] PROGMEM = {     ${freqs.join(',\n    ')} };  const uint16_t melodyDurations[] PROGMEM = {     ${durations.join(',\n    ')} };  const uint16_t melodyDelays[] PROGMEM = {     ${delays.join(',\n    ')} };  void activateCoil(uint8_t pin1, uint8_t pin2) {   digitalWrite(pin1, HIGH);   digitalWrite(pin2, LOW);   delayMicroseconds(300);   digitalWrite(pin1, LOW);   digitalWrite(pin2, LOW); }  void playNote(uint16_t freq, uint16_t dur) {     if (freq < MIN_FREQ || freq > MAX_FREQ || dur < 5) {         delay(dur);         return;     }          uint32_t period = 1000000 / freq;     uint32_t elapsed = 0;     uint32_t durationMicros = dur * 1000L;          while (elapsed < durationMicros) {         uint32_t start = micros();                  activateCoil(COIL_A1, COIL_A2);         delayMicroseconds(period/2 - 300);         activateCoil(COIL_B1, COIL_B2);         delayMicroseconds(period/2 - 300);                  elapsed += micros() - start;     } }  void setup() {     pinMode(COIL_A1, OUTPUT);     pinMode(COIL_A2, OUTPUT);     pinMode(COIL_B1, OUTPUT);     pinMode(COIL_B2, OUTPUT); }  void loop() {     for (uint16_t i = 0; i < ${notes.length}; i++) {         uint16_t freq = pgm_read_word(&melodyFreqs[i]);         uint16_t dur = pgm_read_word(&melodyDurations[i]);         playNote(freq, dur);                  uint16_t del = pgm_read_word(&melodyDelays[i]);         if (del > 0) delay(del);     } }`;        elements.downloadBtn.disabled = false;       elements.copyBtn.disabled = false;     }      function copyToClipboard() {       elements.codeOutput.select();       document.execCommand('copy');       showStatus("Код скопирован в буфер обмена!");       setTimeout(() => showStatus("Готово"), 2000);     }      function downloadCode() {       const blob = new Blob([elements.codeOutput.value], { type: 'text/plain' });       const url = URL.createObjectURL(blob);       const a = document.createElement('a');       a.href = url;       a.download = 'motor_music.ino';       document.body.appendChild(a);       a.click();       document.body.removeChild(a);       URL.revokeObjectURL(url);     }      function showStatus(message, isError = false) {       elements.status.textContent = message;       elements.status.className = isError ? 'status status-error' : 'status status-info';     }      function init() {       document.addEventListener('click', function initAudioOnClick() {         initAudio();         document.removeEventListener('click', initAudioOnClick);       }, { once: true });        elements.midiUpload.addEventListener('change', async function(e) {         const file = e.target.files[0];         if (!file) return;                  if (!audioContext) {           showStatus("Ошибка: сначала кликните по странице для активации звука", true);           return;         }                  showStatus("Загрузка MIDI...");         elements.downloadBtn.disabled = true;         elements.copyBtn.disabled = true;         elements.playBtn.disabled = true;         elements.stopBtn.disabled = true;                  try {           const arrayBuffer = await file.arrayBuffer();           currentMidiData = parseMidi(arrayBuffer);                      elements.midiInfo.innerHTML = `             <strong>${file.name}</strong><br>             Дорожек: ${currentMidiData.tracks.length}<br>             Нот: ${currentMidiData.totalNotes}<br>             Длительность: ${formatTime(currentMidiData.duration)}<br>             Темп: ${bpm} BPM           `;                      createTrackSelection(currentMidiData.tracks);           processMidi(currentMidiData);                      elements.playBtn.disabled = false;           elements.timeDisplay.textContent = `${formatTime(selectedStartTime)} / ${formatTime(selectedEndTime)}`;           showStatus("MIDI загружен. Нажмите 'Воспроизвести'");         } catch (err) {           console.error("Ошибка:", err);           showStatus("Ошибка: " + err.message, true);         }       });        elements.playBtn.addEventListener('click', playMidi);       elements.stopBtn.addEventListener('click', stopPlayback);              elements.minFreq.addEventListener('input', handleChanges);       elements.maxFreq.addEventListener('input', handleChanges);       elements.tempo.addEventListener('input', handleChanges);       elements.volume.addEventListener('input', handleChanges);       elements.filterDrums.addEventListener('change', handleChanges);       elements.trimStart.addEventListener('input', handleChanges);       elements.trimEnd.addEventListener('input', handleChanges);              elements.copyBtn.addEventListener('click', copyToClipboard);       elements.downloadBtn.addEventListener('click', downloadCode);              updateSliderValues();       showStatus("Кликните по странице для активации звука");     }      init();   </script> </body> </html>

Итак, теперь, у вас есть инструмент, который позволяет достаточно легко внедрять музыку, в ваши любительские проекты — нужно только запустить генератор, загрузить туда midi-трек, скачать или скопировать сгенерированный код и использовать в своих проектах! 

Исходников, то бишь midi-файлов в сети можно найти великое множество.

Ну что, остаётся только сказать «а-аай, арриба» и достать из широких штанин свои маракасы с полки шаговый двигатель?! 🙂


ссылка на оригинал статьи https://habr.com/ru/articles/935790/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *