MIDI chiptune + detune
Добавляем функциональность к музыкальному MIDI-синтезатору на двух чипах AY-3-8910, а также вспоминаем, что общего между музыкой и математикой.
Оглавление
Когда-то у меня был компьютер ZX Spectrum с музыкальным сопроцессором AY-3-8910 (правильнее его называть PSG), и в то время качество его звучания впечатляло. Даже когда стали широко распространены IBM PC, которые по вычислительным возможностям на порядки превосходили Speccy, подобных музыкальных возможностей в них поначалу не было, а первые звуковые карты стоили дорого. Воспоминания о тех временах свежи в памяти, и звук этого PSG мне нравится до сих пор, поэтому различные статьи и проекты на тему использования музыкальных чипов AY-3-8910/8912 вызывают интерес и сейчас.
На Хабре есть материалы, связанные с воспроизведением готовой музыки из файлов с трекерной музыкой на этих чипах, но мне хочется затронуть тему воспроизведения музыки на них с использованием MIDI-интерфейса. Например, собрать устройство, к которому можно подключить MIDI-клавиатуру и самому играть музыку, либо воспроизводить ее из программы-секвенсора в компьютере. Для реализации я выбрал платформу Arduino, которая для меня оказалась оптимальной по простоте освоения и возможностям, и которой более чем достаточно для данной задачи.
В сети нашлось некоторое количество готовых проектов на эту тему — например, такой. Это довольно интересный синтезатор с неплохими возможностями, но на одном чипе AY-3-8910, у которого всего 3 звуковых канала синтеза, поэтому играть полифонию из более чем трех одновременно звучащих нот на нем невозможно. Автор другого проекта решил поставить два чипа, чтобы увеличить полифонию до 6 голосов. Это уже интереснее - проанализируем, что у него получилось.
Dual AY-3-8910 MIDI module
Описание проекта вместе с кодом скетча для Arduino находится здесь. Для начала рассмотрим принципиальную схему устройства.
Устройство состоит из следующих частей:
Arduino Pro Micro
2 чипа AY-3-8910 (далее я их буду называть просто AY)
Звуковой линейный моно-выход (в него объединены все выходные каналы чипов AY)
MIDI-вход на оптронной паре (о том, почему его часть обведена красным, будет ниже)
Существуют разные типы устройств Arduino, отличающиеся размерами, чипом микроконтроллера и т.д. Большинство проектов (особенно обучающих) с использованием данной платформы, описания которых можно найти в сети, реализованы на Arduino Uno. В данном же устройстве используется Arduino Pro Micro – аналог Arduino Leonardo. Мне данный тип нравится своей компактностью и дешевизной, но главное здесь не в этом. При подключении такого устройства по USB в компьютере появляется MIDI-устройство, которое можно выбрать в качестве выходного устройства в программном секвенсоре и играть через него музыку. При этом, MIDI-команды будут передаваться из компьютера непосредственно в Arduino через виртуальный последовательный порт.
Одним из недостатков Arduino является небольшое количество пинов (контактов), которые можно использовать для подачи сигналов к управляемым устройствам. Для управления чипом AY используются 8 контактов (сигналов) шины данных DA0...DA7, сигнал сброса Reset, управляющие сигналы BC1, BC2 и BDIR, а также сигнал Clock для подачи тактовой частоты генератора. Для генерации сигнала тактовой частоты, как и во многих аналогичных проектах, используется аппаратный таймер Arduino. Этот сигнал (в данном случае с частотой 1 МГц) подается на контакты Clock обоих чипов AY. Сигнал сброса Reset также подключен параллельно к обоим чипам, т. к. в рамках текущей задачи нет необходимости независимо сбрасывать состояние двух AY.
Но если подключать остальные сигналы для управления чипами полностью независимо, это потребует минимум 20 контактов, а такого количества свободных пинов у Arduino нет. Автор решил эту проблему следующим образом: 8 пинов Arduino подключил параллельно к контактам DA0...DA7 шины данных обоих чипов AY, сигналы BC1 обоих чипов заземлил (используется только запись в регистры), а сигналами BC2/BDIR обоих чипов управляет независимо. В коде программы автор назвал соответствующие пины Arduino как BC2_A/BDIR_A и BC2_B/BDIR_B для управления записью данных в чипы AY, первый из которых он условно назвал A, а второй B.
Для записи данных в регистр чипа A в коде предназначена процедура writeReg_A, а для записи в регистр чипа B – процедура writeReg_B. Код в них одинаков, за исключением того, что управляющие сигналы для передачи данных подаются на пины BC2_A/BDIR_A и BC2_B/BDIR_B соответственно. Подробно рассматривать принципы управления чипом AY я не буду — это описано в даташите и описании автора проекта (при желании можно найти материалы и на русском языке). Отмечу лишь, что для задания высоты звучания ноты используется тактовая частота (в данном случае 1 МГц), которая делится на 16, а затем — на дополнительный делитель, значение которого и задает нужную частоту звучания.
Принцип работы
Главное достоинство устройства — полифония на 6 голосов. Звуковые выходы (каналы синтезатора) A/B/C обоих чипов AY объединены в один моно-выход. Стерео здесь делать особого смысла нет, т. к. в общем случае непонятно, как автоматически распределять ноты, поступающие по MIDI, по левому/правому каналам. Особенно, если несколько нот играется одновременно, но включаться и выключаться они могут в произвольном порядке.
В общих чертах алгоритм работы устройства выглядит следующим образом — в главном цикле loop 100 раз в секунду осуществляется последовательность действий:
Опрашивается USB MIDI-интерфейс. Если из него получено MIDI-сообщение, оно обрабатывается (процедура handleMidiMessage).
Опрашивается аппаратный MIDI-интерфейс. Если из него получено MIDI-сообщение, оно обрабатывается (процедура handleMidiMessage).
По результатам обработки сообщений выключаются/включаются ноты нужной частоты и громкости в 6 синтезируемых каналах двух чипов AY. Для этого формируются необходимые значения для регистров AY, и если они отличаются от предыдущих (сохраненных в отдельные переменные), то они записываются в регистры.
Обрабатываются не все типы MIDI-сообщений, а только Note on (включение ноты), Note off (выключение ноты) и Control change (для полного выключения звука по командам AllSoundOff, ResetAllControllers, AllNotesOff). Помимо включения музыкальных тонов, с помощью шумовых генераторов чипа AY эмулируется звучание ударных инструментов. Также реализовано автоматические изменение громкости нот с помощью огибающих. В частности, после выключения ноты она еще некоторое время звучит с затуханием громкости — получается более естественное звучание, похожее на реальный инструмент (например, фортепиано).
Самая сложная часть кода — автоматическое распределение нот по свободным каналам синтезатора. Автор проделал хорошую работу — написал алгоритм, который при необходимости включить очередную ноту сначала ищет среди 6 каналов синтезатора свободный, и если не находит, принудительно выключает самую «неважную» из уже играющихся нот. Здесь приходится идти на компромиссы - самыми «важными» нотами считаются самые нижние и самые высокие по звучанию, и они могут быть принудительно выключены только в последнюю очередь. Можно поспорить о правильности такого подхода, но в большинстве случаев, действительно, басы являются важной основой музыкальной композиции, а в самых верхних нотах часто содержится сольная часть мелодии, поэтому выключать их тоже нежелательно.
Первый запуск
После того, как я спаял устройство, подключил по USB к компьютеру и запрограммировал с помощью Arduino IDE (для того, чтобы код собрался, необходимо установить библиотеки MIDIUSB и Arduino MIDI), оно даже сразу заработало. Для проверки я использовал программу-секвенсор Cakewalk (уже несколько лет как бесплатную). В окне выбора выходных MIDI-устройств наш синтезатор выглядит в списке как Arduino Leonardo:
Теперь можно вызвать виртуальную MIDI-клавиатуру и «поиграть» на ней мышью или запустить воспроизведение MIDI-файла:
Как видим, всё работает, и даже встроенный светодиод RX мигает, когда поступают данные. Но когда я подключил к аппаратному MIDI-входу музыкальную клавиатуру и попытался сыграть на ней — в ответ тишина. После некоторых раздумий я полез в сеть и поискал даташит на 6N137 и схемы подключения MIDI-входа к Arduino. Мои подозрения подтвердились — автор немного напутал выводы оптрона. После того, как я заменил обведенную красным часть схемы с оптроном на аналогичную часть отсюда, заработало и получение нажатий клавиш с MIDI-клавиатуры. Правильно эта часть схемы выглядит так (для надежности еще добавлен конденсатор между землей и питанием оптрона):
После того, как устройство полностью заработало, выявились и некоторые его недостатки, вызвавшие у меня желание доработать его.
Первая доработка
На MIDI-клавиатуре, помимо нотных клавиш, есть еще колеса-регуляторы Pitch Bend и Modulation:
Pitch Bend позволяет плавно изменять высоту играемой ноты вверх-вниз (обычно до полутона), а Modulation позволяет включить периодическое «плавание» (вибрато) высоты ноты вверх-вниз (чем больше отклонение от нейтрального положения, тем сильнее «плавает» высота). Но здесь они бездействуют, т.к. в коде скетча для Arduino нет обработки соответствующих сообщений. Я захотел добавить обработку этих регуляторов.
Можно было найти и изучить описание стандарта MIDI, но я поступил проще. Автор предусмотрел распечатку в отладочный COM-порт всех поступающих со входа MIDI-сообщений. Для этого достаточно убрать комментарий в строке #define DEBUG и перепрограммировать Arduino. После этого я подвигал колесо Pitch Bend и увидел, что поступают сообщения с заголовком 0x0E, 0xE0 и значением отклонения в байте 3 в диапазоне от 0x00 до 0x7F (в нейтральном положении значение 0x40). А если подвигать колесо Modulation, то поступают сообщения с заголовком 0x0B, 0xB0, 0x01 и значениями от 0x00 (нет вибрато) до 0x7F (максимальное вибрато) в байте 3. Стало понятно, что в процедуре handleMidiMessage нужно добавить обработку этих двух сообщений и соответственно изменить вычисление частоты ноты, поступившей в сообщении Note on.
Вторая доработка
Как известно, чип AY выдает в каждом канале синтеза прямоугольный звуковой сигнал. В принципе, он уже звучит приятнее, чем просто синус, но все равно достаточно «плоско». Это можно заметить в видеоролике, который приведен выше. Хочется сделать звук более «интересным». В чипе AY нет возможности выбрать другую форму синтезированного сигнала. Но можно попробовать смешать два сигнала при воспроизведении каждой ноты — тем более, что у нас не один чип, а два. Это уменьшит полифонию до трех голосов, но если играть партию на клавиатуре, используя синтезатор как солирующий инструмент, для определенного круга задач этого вполне достаточно.
Если смешать два сигнала с одинаковой частотой, звук станет просто громче в 2 раза. Но если у второго сигнала частоту немного изменить относительно первого, получится более «сочный» звук. Такой эффект обычно называют «detune» (можно перевести как «расстройка музыкального тона»). Можно найти примеры видео, в которых показано, как меняется звук, если крутить ручку «detune» у синтезатора. Чем больше ее отклонение от нуля, тем больше отклонение частоты второго сигнала и заметнее отличие, и тем «сочнее» получается звук (но если перебрать с отклонением, звук начнет сильно искажаться).
Изменение схемы и анализ алгоритма
Чтобы добавить функцию detune, я решил добавить в схему дополнительную кнопку, нажатие которой включает режим смешивания двух звуковых сигналов при включении ноты (а повторное нажатие возвращает исходный режим). В таком режиме алгоритм поиска свободного канала синтезатора нужно ограничить до 3 голосов вместо 6, а при включении звучания ноты в свободном канале первого чипа AY одновременно включать звучание ноты в аналогичном канале второго чипа AY, но с немного другой частотой. Для регулировки отличия частоты второго сигнала от первого я добавил в схему переменный резистор. Как видно по схеме, у Arduino Pro Micro есть еще два свободных пина — A2 и A3, их как раз достаточно для подключения дополнительной кнопки и регулятора:
Для того, чтобы понять, какие изменения нужно внести в алгоритм, понадобился его тщательный анализ. Были выделены процедуры, в которых осуществляется вычисление частоты ноты, которую нужно включить, распределение одновременно воспроизводимых нот по 6 доступным каналам для воспроизведения и собственно включение/выключение нот.
В коде автора имеется таблица note_table с фиксированными значениями делителя тактовой частоты генератора 1 МГц, чтобы с ее помощью получать звуки с частотами нужных нот из MIDI-диапазона. Стало понятно, что первым делом нужно отказаться от этой таблицы и заменить ее на таблицу частот, т. к. для реализации Pitch Bend, модуляции и detune необходимо включать звучание нот не с фиксированными частотами, а пересчитанными "на лету". Второе изменение — если включен режим detune, когда нужно к основной звучащей ноте добавлять еще одну одновременно звучащую ноту, то необходимо распределять ноты при воспроизведении не по всем 6 каналам, а только по трем.
Немного о музыке и математике
Чтобы реализовать первую доработку (и еще одну, о которой пойдет речь ниже), понадобится немного вычислений. Одна из особенностей восприятия звука человеком заключается в том, что для повышения высоты тона на октаву необходимо увеличить частоту звукового сигнала в 2 раза. Например, нота «ля» первой октавы имеет частоту примерно 440 Гц, а нота «ля» второй октавы — частоту 880 Гц. Как получить коэффициент (обозначим его S), на который нужно умножить частоту, чтобы увеличить высоту на один полутон? В октаве 12 полутонов. Поэтому, если умножить частоту на этот коэффициент 12 раз, должно получиться удвоенное значение частоты. В результате S равен корню двенадцатой степени из 2:
Примечание: здесь и далее подразумевается, что используется так называемый равно-темперированный строй, который в большинстве случаев используется и в электронной музыке, и во многих музыкальных инструментах. Подробнее об этом можно прочитать, например, здесь.
Чтобы реализовать функцию Pitch Bend, нужно получить значение еще одного коэффициента. Как мы помним, при отклонении колеса от нейтрального положения поступают сообщения Pitch Bend со значением текущего отклонения. В нейтральном положении значение отклонения равно 0x40. При отклонении вверх возможны значения от 0x41 до 0x7F (максимальное повышение тона), а при отклонении вниз — от 0x3F до 0x00 (максимальное понижение тона). Для простоты будем считать, что при отклонении колеса до максимального положения что вниз, что вверх возможны 63 значения (шага) этого отклонения. Поэтому, нужно рассчитать коэффициент (назовем его pf), на который нужно умножить частоту звучания ноты, чтобы повысить его на 1/63 полутона. При максимальном отклонении колеса частоту нужно будет умножить на этот коэффициент 63 раза, и получить коэффициент S — при этом нота повысится на полутон. Очевидно, что pf равен корню 63-й степени из S:
Чтобы рассчитать частоту F звучания ноты с учетом отклонения колесом Pitch Bend, нужно провести следующие вычисления: от значения отклонения колеса B из MIDI-сообщения отнять 0x40 (нейтральное значение), возвести величину pf в степень с полученным значением и умножить частоту "чистой" ноты F0 на результат:
Функция Modulation (вибрато) реализуется с помощью аналогичных вычислений, только значение дополнительного отклонения тона формируется автоматически с помощью периодической функции. Амплитуда этой функции умножается на текущее значение, полученное из MIDI-сообщения Modulation. Текущее значение отклонения умножается на значение текущего шага модуляции, который постоянно пересчитывается при обновлении состояния 100 раз в секунду.
Для реализации функции detune дополнительное отклонение высоты тона необходимо прочитать из АЦП, к которому подключен переменный резистор.
Изменения в коде
Чтобы реализовать все задуманное, в код были внесены следующие изменения:
добавлены объявления номеров пинов, к которым подключены переменный резистор для регулировки степени detune и кнопка переключения режимов работы
добавлены переменные для хранения текущий значений: g_pitchBend — отклонение высоты тона, g_modDepth - глубина вибрато, а также переменная для хранения номера текущего режима g_detuneType
список возможных значений режимов задан в enum eDetune, где значение eNoDetune означает обычный режим работы, как раньше (при этом возможна полифония до 6 голосов, а в режимах с включенным detune доступно только до 3 голосов)
добавлен класс CBtn для обработки нажатий кнопки с устранением дребезга контактов
таблица note_table с фиксированными значениями делителя базовой частоты звукового генератора для каждой из нот в диапазоне стандарта MIDI заменена на таблицу freq_table со значениями частот тех же нот (каждое значение умножено на 10 для повышения точности)
в класс Voice добавлены переменные: m_note для хранения текущей ноты, m_modstep для хранения текущего шага модуляции (вибрато), m_detune для хранения текущего режима воспроизведения ноты
в функции start класса Voice получение значения делителя базовой частоты из таблицы заменено на вычисление в функции getPitch, также сбрасывается в 0 текущий шаг модуляции m_modstep
добавлена функция getPitch, вычисляющая значение делителя с учетом режима detune, текущих значений отклонения высоты тона и вибрато в переменных g_pitchBend и g_modDepth, а также значения шага вибрато в переменной m_modstep
в функции update100Hz класса Voice добавлена обработка вибрато, а также вычисление делителя базовой частоты на каждом шаге, чтобы вращение регулятора степени detune и колес Pitch Bend и Modulation обрабатывались даже в процессе воспроизведения ноты
в функции startNote теперь проверяется текущий режим работы, и если он отличен от eNoDetune, максимальное количество голосов, в которых может быть включено звучание ноты, уменьшается с 6 до 3, и при включении основной ноты также включается дополнительная нота с включенным режимом detune
в функции включения звука ударного инструмента startPercussion также проверяется текущий режим работы, но только для ограничения максимального количества голосов с 6 до 3
аналогичная проверка режима добавлена в функции выключения «лишней» ноты stopOneNote
в функции выключения звучащей ноты stopNote, если текущий режим отличен от eNoDetune, помимо выключения основной ноты выключается также дополнительная нота с включенным ранее режимом detune
в функции setup добавлена инициализация входных пинов для получения значений с переменного резистора и обработки нажатий кнопки переключения режимов, а также выходного пина для включения/выключения встроенного светодиода TX
в функции handleMidiMessage добавлены: обработка сообщения Pitch Bend (текущее значение отклонения высоты тона сохраняется в переменную g_pitchBend); обработка кода Modulation в сообщении Control Change (текущая глубина вибрато сохраняется в переменную g_modDepth); цикл выключения всех звучащих голосов вынесен в отдельную функцию KillVoices (также в нее добавлен вызов synth_init, которая сбрасывает все внутренние переменные, ответственные за «занятость» каналов и распределение нот по ним)
в функции главного цикла работы loop добавлена обработка нажатия кнопки btnDetune — если она нажата, производится принудительное выключение всех голосов вызовом KillVoices, а затем переключение текущего режима работы в переменной g_detuneType на следующий, а если он отличен от eNoDetune, то включается встроенной светодиод TX для индикации включенного режима detune
Третья доработка
После того, как я реализовал задуманное и проверил в работе, в голову пришла еще одна идея. А что, если при включенном режиме detune добавлять вторую ноту не той же высоты, а на октаву выше или ниже? Это должно дать еще более «сочный» звук — такой эффект обычно называют «октавер». Для этого достаточно частоту ноты дополнительно умножить либо поделить на 2. Потом я решил добавить еще два режима: добавление ноты, сдвинутой не на октаву, а на кварту (5 полутонов) или квинту (7 полутонов) относительно основной ноты. В результате получился набор из целых пяти режимов detune – в коде они перечислены в enum eDetuneType.
Вычисление частоты ноты
Рассмотрим итоговую процедуру getPitch, с помощью которой вычисляется значение делителя частоты с учетом отклонения, модуляции и detune.
Текст процедуры
typedef double freq_t;
const freq_t ayf = 625000.0,
pf = 1.0009172817958015637819657653483, // (2^(1/12))^(1/63)
pf5 = 1.3348398541700343648308318811845, // (2^(1/12))^5
pf7 = 1.4983070768766814987992807320298, // (2^(1/12))^7
dr = 50.0; // detune ratio coefficient
const ushort modlength = 20, modmax = 10; // modulation rate and max depth
ushort getPitch(note_t note, eDetune detune, note_t modstep) {
freq_t freq = freq_table[note - MIDI_MIN], fp = 1.0;
int modval = (modstep > modlength / 2) ? modlength - modstep : modstep,
pitchBend = g_pitchBend + modval * modmax * g_modDepth / 0x7F;
if (pitchBend != 0)
fp = pow(pf, freq_t(pitchBend));
if (detune > eNoDetune) {
fp *= pow(pf, freq_t(analogRead(PIN_DETUNE_RATIO)) / dr);
switch (detune) {
case eDetuneOctUp:
fp *= 2.0;
break;
case eDetuneOctDn:
fp *= 0.5;
break;
case eDetune5:
fp *= pf5;
break;
case eDetune7:
fp *= pf7;
break;
}
}
freq *= fp;
float divider = float(ayf / freq);
return (ushort)divider;
}
Примечание: для вычисления частоты введен дополнительный тип данных freq_t, чтобы при необходимости можно было заменить его. В моем случае этот тип равносилен double.
В таблице freq_table представлены значения частот для каждой из нот MIDI-диапазона (для повышения точности все значения умножены на 10). Сначала в переменную freq копируется значение «чистой» частоты из этой таблицы для текущей ноты. Эта частота будет умножена на коэффициент fp, в начале процедуры в нем значение 1. В зависимости от режима работы и текущих значений всех параметров, этот коэффициент нужно пересчитать.
В переменной modval рассчитывается текущее отклонение высоты тона в соответствии с текущим шагом модуляции modstep, который пересчитывается 100 раз в секунду. Скорость и максимальная амплитуда модуляции в виде пилообразной функции заданы константами modlength и modmax (подобраны экспериментально, можно их изменить по своему усмотрению).
В переменной pitchBend рассчитывается суммарное отклонение, полученное в результате модуляции и отклонения колесом Pitch Bend. Если оно не равно нулю, значение коэффициента fp рассчитывается как pf в степени pitchBend.
Если текущий режим отличен от eNoDetune, нужно добавить небольшое отклонение частоты, полученное из положения переменного резистора. Для этого fp дополнительно умножается на коэффициент, полученный возведением pf в степень значения, считанного из пина, к которому подключен переменный резистор. АЦП на пинах Arduino 10-битный, выдает значения от 0 до 1023. Опытным путем установлено, что считанное значение нужно разделить на коэффициент dr, равный 50, иначе при максимальном отклонении резистора искажения звука слишком велики. Значение подобрано экспериментально, также можно его изменить.
В зависимости от режима detune коэффициент fp дополнительно умножается на 2 или 0.5 (повышение или понижение на октаву), либо на 2 в степени 5/12 или 7/12 (повышение на кварту или квинту).
В конце процедуры значение «чистой» частоты ноты умножается на итоговое значение коэффициента fp, и для получения итогового значения делителя частоты нужно разделить константу 625000 на полученное значение freq. Когда полученное значение делителя будет внесено в регистры, задающие делитель относительно тактовой частоты, на выходе соответствующего звукового канала AY будет включен тон нужной высоты.
Итоговое устройство
Фото готового устройства представлено в заголовке статьи, а его работа во всех реализованных режимах представлена в видеоролике. Линейный выход подключен к активным колонкам, к MIDI-входу подключена клавиатура — можно играть:
Полный код скетча для Arduino Pro Micro представлен в репозитории.
Перспективы
На этом этапе я пока остановился. В дальнейшем в устройство хотелось бы добавить следующее:
запоминание текущего режима работы в энергонезависимой памяти (EEPROM)
дисплей (OLED или LCD), на котором показываются текущий режим работы, уровни громкости в каналах AY, информация о воспроизводимых нотах во всех каналах, положения регуляторов Pitch Bend, Modulation, ручки Detune и т.п.
возможность регулирования величины максимального отклонения тона колесом Pitch Bend, а также глубины и скорости модуляции (например, дополнительными кнопками или энкодерами) и диапазона ручки Detune, также с запоминанием всех значений в EEPROM
разнообразить звучание какими-то готовыми звуковыми эффектами (например, как в играх для ZX Spectrum)
поставить больше чипов AY для увеличения полифонии (или, например, играть на одном чипе готовые трекерные мелодии, а на остальных — сопровождение с MIDI-клавиатуры)
Наверное, можно придумать что-то еще. Если у кого-то есть идеи, предлагайте их в комментариях. Также, я не являюсь специалистом по MIDI, поэтому, если кто-нибудь обнаружил неточности или возможные проблемы в обработке MIDI-сообщений в коде программы, прошу сообщить об этом.
Ссылки по теме
Upd: опубликовано продолжение про использование генератора огибающей.