
Сегодня мы займёмся одной интересной затеей, которая пришла мне в голову, уже достаточно давно, когда я впервые увидел, как воспроизводят музыку на двигателях, в частности, играют Имперский марш из Звёздных войн, на приводах 3,5-дюймовых дискет, и не только, посылая с помощью микроконтроллера, высокочастотные сигналы на двигатель, издающий при этом звук.
Только, обычно, этот звук двигателей является отрицательным явлением, благодаря чему пользователям даже приходится устройство с этими двигателями (например, ЧПУ-станок или 3D принтер), ставить в другую комнату, чтобы они не докучали.
Мы же заставим этот звук служить нашим интересам, ублажая наши чресла наш слух. :-D
Посему: а сделаем ка, универсальный конвертер/генератор музыки, для игры на двигателях! Никто ведь не против? Нет? Ок, тогда поехали...:-D
Что, как и зачем
Немного продолжая предысторию, можно тут ещё сказать, что подобный подход — воспроизведение «специального», не сугубо технического звука, двигателями, довольно распространён: в частности, подобным образом, дроны бытового назначения, обычно информируют своего пользователя, о разных событиях: включение, переход из одного пункта меню в другое (на пульте управления, во время настроек) и т.д.
При этом, бесколлекторные двигатели дронов издают очень звонкие и громкие звуки (мелодию), специально предназначенную для информирования пользователя (обычно это происходит на начальном этапе, во время настроек и включения).
Если попытаться вникнуть в то, как можно реализовать издание звука двигателями, то там будет несколько возможных вариантов, различающихся возможностями.
Среди них, я бы выделил два наиболее очевидных варианта:
-
оцифровка какого-либо звука или музыки, с определённым битрейтом (этим параметром можно регулировать качество), и сохранение этой звуковой дорожки в микропроцессорное устройство, которое и будет управлять процессом проигрывания.
Способ довольно очевидный, дающий достаточно широкие возможности, однако, основным его минусом (также очевидным) являются потенциальные проблемы с хранением большого объёма информации, который этот способ сгенерирует.
воспроизведение музыки, в формате midi: для микроконтроллеров с малым объёмом памяти это способ видится одним из самых интересных: в его рамках, надо оперировать только ограниченным числом нот и последовательностью их воспроизведения, варьируя, кроме этого, продолжительностью их звучания, и промежутками между ними.
После ряда размышлений над всем этим, я решил остановиться именно на втором варианте, так как целей генерации речи или полноценной музыки, я себе изначально не ставил.
Как показали дальнейшие эксперименты, этот выбранный путь (в рамках обозначенных целей — воспроизведения простых звуков), оказался более чем верным: я пытался загружать разные треки в микроконтроллер, однако, несмотря на то, что некоторые треки были длиной в минуту и более, — они у меня никогда не занимали где-то более 15-18% памяти микроконтроллера! Неплохой результат! :-)
Кстати о микроконтроллере: в качестве объекта для загрузки, я выбрал микроконтроллер esp32 wroom32, которому был подключен драйвер двигателя, где, уже к нему, был подключен шаговый двигатель типоразмера Nema 17.
То есть, другими словами, я решил играть музыку на шаговом двигателе — продолжить, так сказать, «традиции олдов» :-)
Кроме того, уже изначально, я поставил себе следующую цель: не хочется, просто создать очередное конкретное решение, с конкретной музыкой, пройдя сложный путь, разбираясь во всех тонкостях, и, если кто-то захочет пройти этим же путём — фактически вынуждая его разобраться во всём этом вале информации самостоятельно (а кто-то и не сможет, или не захочет).
Вместо этого, я решил кардинально упростить всё: сделать универсальное решение, используя которое, любой желающий, сможет легко и быстро создать музыкальный трек для своего шагового двигателя — взяв за основу midi-файл из интернета, подкорректировав его (то есть, вырезав из этого трека нужный фрагмент, который должен воспроизводиться на двигателе или оставив весь трек целиком) и, преобразовав его — получить автоматически файл, для загрузки в микроконтроллер.
Кстати, тут надо ещё отметить такой момент, а зачем это вообще может понадобиться кому-то (играть музыку с помощью двигателя)?
На мой взгляд, это новая интересная возможность, которой могут воспользоваться многие: скажем, использовать сгенерированный код для вставки в свой проект, где играющая с помощью двигателя музыка, будет информировать о разных этапах или режимах.
Например, как вам понравится такое: загрузить трек из какой-нибудь компьютерной игры, который будет воспроизводиться на двигателе ЧПУ станка или самодельного робота, после завершения работы — какой-нибудь «mission complete» или «stage clear»-саундтрек, из компьютерной игры.
Или, скажем, издание двигателем звуков, синхронно, с переходом юзера из одного меню дистанционного беспроводного пульта управления — в другой/переключение режимов и т.д.
Или даже проигрывание грустной музыки, в стиле «game over» — если что-то пошло не так! :-D
Так что тут возникает много интересных возможностей — ограниченных только вашей фантазией...
К тому же, такой подход (музыка на двигателях) очень редко применялся ранее в самодельных проектах (в основном, насколько я понимаю, в виду сложности и комплексности задачи, в которой необходимо разобраться, чтобы успешно создать такое решение) — ведь необходимо было понимать не только в программировании, но и хорошо знать теорию музыки и, понять, как это теперь всё соединить воедино...
Поэтому, ранее, подобным баловались только 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-файлов в сети можно найти великое множество.
Ну что, остаётся только сказать «а-аай, арриба» и достать из широких штанин свои маракасы с полки шаговый двигатель?! :-)
Prohard
А как с примерами звучания шагового двигателя?
cnet Автор
Вначале добавил в конец статьи - потом снёс :-D, дабы не нарушать права кое кого.
Но в личку могу кинуть.
UPD.Кинул в личку