Сегодня мы займёмся одной интересной затеей, которая пришла мне в голову, уже достаточно давно, когда я впервые увидел, как воспроизводят музыку на двигателях, в частности, играют Имперский марш из Звёздных войн, на приводах 3,5-дюймовых дискет, и не только, посылая с помощью микроконтроллера, высокочастотные сигналы на двигатель, издающий при этом звук.
Только, обычно, этот звук двигателей является отрицательным явлением, благодаря чему пользователям даже приходится устройство с этими двигателями (например, ЧПУ-станок или 3D принтер), ставить в другую комнату, чтобы они не докучали.
Мы же заставим этот звук служить нашим интересам, ублажая наши чресла наш слух. 😀
Посему: а сделаем ка, универсальный конвертер/генератор музыки, для игры на двигателях! Никто ведь не против? Нет? Ок, тогда поехали…:-D
Что, как и зачем
Немного продолжая предысторию, можно тут ещё сказать, что подобный подход — воспроизведение «специального», не сугубо технического звука, двигателями, довольно распространён: в частности, подобным образом, дроны бытового назначения, обычно информируют своего пользователя, о разных событиях: включение, переход из одного пункта меню в другое (на пульте управления, во время настроек) и т.д.
При этом, бесколлекторные двигатели дронов издают очень звонкие и громкие звуки (мелодию), специально предназначенную для информирования пользователя (обычно это происходит на начальном этапе, во время настроек и включения).
Если попытаться вникнуть в то, как можно реализовать издание звука двигателями, то там будет несколько возможных вариантов, различающихся возможностями.
Среди них, я бы выделил два наиболее очевидных варианта:
-
оцифровка какого-либо звука или музыки, с определённым битрейтом (этим параметром можно регулировать качество), и сохранение этой звуковой дорожки в микропроцессорное устройство, которое и будет управлять процессом проигрывания.
Способ довольно очевидный, дающий достаточно широкие возможности, однако, основным его минусом (также очевидным) являются потенциальные проблемы с хранением большого объёма информации, который этот способ сгенерирует.
-
воспроизведение музыки, в формате 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/
Добавить комментарий