Данная статья серии посвящена использованию протокола MIDI для воспроизведения музыки на устройстве, построенном на микроконтроллере ATtiny2313. В ней я расскажу о приёме MIDI-сообщений для управления ранее разработанным синтезатором.
Введение
Протоколу MIDI (Musical Instrument Digital Interface, дигитальный интерфейс для музыкальных инструментов) в 2013 году исполняется 30 лет. Однако он настолько хорошо удовлетворяет потребности музыкантов и музыкальных инженеров, что вряд ли в ближайшее время он будет вытеснен каким-либо другим стандартом.
Статья организована так: сперва я расскажу, как можно подключить два MIDI-устройства друг к другу (рассмотрим вариант ATtiny2313 и ПК), затем в двух словах опишу основные используемые в стандарте команды, и, наконец, покажу как можно ранее разработанное устройство подружить с MIDI.
Связь с ПК посредством MIDI
Строго говоря, для использования MIDI протокола желательно соответствующее оборудование. Например, аудио-карта, оснащённая MIDI-интерфейсом (здесь до сих пор используется популярный некогда DIN5), или специальное USB-устройство. Это не всегда под рукой, а использовать MIDI всё-таки хочется. Тут можно вспомнить про некий стандарт RS-232 и поискать на заднице компьютера COM-порт. У меня на ноутбуке конечно ничего такого не найдёшь, но сгодится и USB-COM конвертер.
О подключении ATtiny2313 к RS-232-совместимому интерфейсу можно писать отдельную статью, но это не входит в мои цели. Напоминаю, что USART-устройство ATtiny2313 выдаёт TTL-уровни (0/5В), а RS-232 хочет -12/+12В в качестве обозначения логических состояний. Поэтому чтобы связать tiny с COM-портом можно использовать либо специальную микросхему (MAX232), либо более дешёвый и простой способ, тут уж смотря какие конечные цели[1].
Общая схема подключения выглядит следующим образом:
Конечно, без определённых ухищрений музыкальные программы напрямую работать с COM-портом не будут. А поэтому надо как-то наладить коммуникацию. На выручку приходит драйвер от компании Roland, подходящий для произвольного устройства и создающий в системе виртуальный MIDI-порт, присылающий и принимающий сообщения от COM-порта (своеобразный мост). Скачать можно на официальном сайте, например для Windows XP можно забрать отсюда, там же на сайте есть подходящий драйвер и для MacOS.
Во время установки надо проследить, чтобы был выбран правильный идентификатор COM-порта. У меня, например, это COM1. Кстати, драйвер от Roland хочет обязательно работать с портами в диапазоне COM1…4, так что в случае назначения системой физическому порту номера 5 и выше, нужно будет изменить эту настройку. Если у вас Windows XP, то порты можно проверить в диспетчере устройств. Если всё правильно настроено, в системе появится дополнительный MIDI порт: входы Roland Serial MIDI Input и два выхода Roland Serial MIDI Out A и B. Согласно этому настраиваем и любую программу, принимающую/отправляющую MIDI-сообщения через COM-порт.
Внимание! Теперь ваш COM-порт будет занят MIDI-приложениями, и для доступа к нему нужно закрыть MIDI-соединения.
Краткое описание MIDI-протокола
Для передачи информации посредством MIDI используется асинхронный последовательный протокол, для пересылки байта нужно 10 бит: стартовый бит, 8 бит информации и стоп-бит. Скорость передачи по стандарту равна 31.25 кбод. Как видно, USART-компонент ATtiny2313 идеально подходит для физической реализации протокола.
Формат сообщений
Вообще говоря, сообщение может состоять как из одного, так и из нескольких байт. Однако первый байт всегда является идентификатором команды (статусный байт):
Старший полубайт (биты 7…4) задают команду, а младший полубайт (биты 3…0) — MIDI-канал, к которому относится команда.
Мы с вами рассмотрим далее сообщения, состоящие из 3 байт информации (статусный байт и два байта данных). Этого вполне достаточно для упрощённой реализации протокола. Полубайты команд буду задавать в шестнадцатиричном представлении. Таким образом, рассматриваемые сообщения выглядят так:
Поговорим теперь по порядку об интересующих нас MIDI-сообщениях.
Note On сообщение — 0X9
Данная команда используется для «включения», то есть начала проигрывания ноты на заданном канале. Она имеет вид 0X9K 0XNN 0XVV
, где K — номер канала (0X0…0XF, то есть от 1 до 16), NN — номер ноты (0X00…0X7F; 0…127), VV — так называемая скорость (velocity) извлечения ноты (0X00…0X7F; 0…127), что соответствует динамике исполнения.
Приведу пример. Допустим на инструмент поступило сообщение 0X91 0X45 0X60
. Это значит, что инструмент должен начать воспроизведение ноты под номером 0X45, что есть нота ля первой октавы (440 Гц) с динамикой 0X60 на втором канале.
Note Off сообщение — 0X8
Эта команда выполняет прямо противоположное действие предыдущей, выключая ноту на заданном канале, при этом её формат 0X8K 0XNN 0XVV
. Поскольку динамика «выключения» учитывается редко, то содержание VV обычно 0X00.
Рассматривая предыдущий пример: для выключения ноты ля первой октавы на втором канале можно отправить сообщение 0X81 0X45 0X00
.
Тут есть одно важное замечание. Дело в том, что спецификация MIDI позволяет заменять сообщения Note Off сообщениями Note On с нулевой динамикой.
Так что выключить ноту в нашем примере можно и при помощи сообщения 0X91 0X45 0X00
. Это важно помнить при программировании MIDI-устройств.
Controller Change (CC) сообщение — 0XB
Сообщения изменения контроллеров используются для изменения таких параметров исполнения, как, например, громкость. Формат следующий: 0XBK 0xNN 0xVV
, где K — номер канала, на котором необходимо изменить параметры контроллера, NN — номер контроллера (0X00…0X7F), VV — значение настройки контроллера (0X00…0X7F).
Нас здесь может заинтересовать сообщение контроллера 0XBK 0x7B 0x00
— «All Notes Off», которое используется для одновременного отключения всех нот на заданном канале K.
Схема устройства
Схема практически не изменилась, разве что добавился коннектор для подключения конвертера уровней, оговоренного выше[2].
Программный код
Перейдём теперь к программированию микроконтроллера. Рассмотрим случай управления двухголосым синтезатором при помощи MIDI. Сначала, как обычно, «шапка» прошивки.
; Проект для ATtiny2313 ; Управляемый MIDI Двухголосый Орган v1 .INCLUDE "tn2313def.inc" .DEF Velocity = R8 .DEF SrSave = R9 .DEF Tone1H = R10 .DEF Tone1L = R11 .DEF Tone2H = R12 .DEF Tone2L = R13 .DEF ToneTH = R14 .DEF ToneTL = R15 .DEF ReadBufferL = R16 .DEF ReadBufferH = R17 .DEF Temp = R18 .DEF Temp1 = R19 .DEF Temp2 = R20 .DEF Temp3 = R21 .DEF Temp4 = R22 .DEF ADDRH = R23 .DEF ADDRL = R24 .DEF RxByte = R25 .DEF ByteCnt = R26 .MACRO OUTI ldi R16,@1 out @0,R16 .ENDM .MACRO ADI subi @0, -@1 .ENDM .MACRO MEMR lds @0, @1 .ENDM .MACRO MEMW sts @0, @1 .ENDM .MACRO MEMWI ldi R16, @1 sts @0, R16 .ENDM .EQU BUFFERSIZE = 3 ; Размер буфера приёмника .EQU VOICENUM = 2 ; Число доступных голосов ; --------- ; Константы ; --------- ; MIDI протокол ; Используем MIDI канал 1 --- ; Note on .EQU NoteOn = 0x90 ; Note off .EQU NoteOff = 0x80 ; Controller change .EQU ControllerChange = 0xB0 ; Контроллеры .EQU AllNotesOff = 0x7B ; --------------------------- ; Самая низкая MIDI-нота (C2 = 36) .EQU LowNote = 36 ; Порты .EQU Voice1 = PB3 .EQU Voice2 = PB4 .EQU LED = PB0 ; -------------- ; Разметрка SRAM ; -------------- .DSEG notebuffer: .BYTE BUFFERSIZE voices: .BYTE VOICENUM ; ---------------- ; Начало программы ; ---------------- .CSEG .ORG 0x0000 RJMP RESET ; Reset Handler RETI ;RJMP INT0 ; External Interrupt0 Handler RETI ;RJMP INT1 ; External Interrupt1 Handler RETI ;RJMP TIM1_CAPT ; Timer1 Capture Handler RJMP TIMER1_COMPA ; Timer1 CompareA Handler RETI ;RJMP TIM1_OVF ; Timer1 Overflow Handler RETI ;RJMP TIM0_OVF ; Timer0 Overflow Handler RJMP USART0_RXC ; USART0 RX Complete Handler RETI ;RJMP USART0_DRE ; USART0,UDR Empty Handler RETI ;RJMP USART0_TXC ; USART0 TX Complete Handler RETI ;RJMP ANA_COMP ; Analog Comparator Handler RETI ;RJMP PCINT ; Pin Change Interrupt RJMP TIMER1_COMPB ; Timer1 Compare B Handler RETI ;RJMP TIMER0_COMPA ; Timer0 Compare A Handler RETI ;RJMP TIMER0_COMPB ; Timer0 Compare B Handler RETI ;RJMP USI_START ; USI Start Handler RETI ;RJMP USI_OVERFLOW ; USI Overflow Handler RETI ;RJMP EE_READY ; EEPROM Ready Handler RETI ;RJMP WDT_OVERFLOW ; Watchdog Overflow Handler
Здесь, пожалуй, отдельных комментариев не требуется. Стандартные макросы, которые хорошо бы выносить для удобства в отдельный файл, и константы. Буфер USART-приёмника составляет 3 байта, ровно столько, сколько нужно для приёма 3-байтовых сообщений NoteOn, NoteOff и ControllerChange, то есть это полностью удовлетворяет упрощённой реализации протокола.
Следом за шапкой идёт стандартная инициализация периферии — это описывать не буду, посмотрите это в полном коде[3]. Реализация двухголосого синтезатора также не изменилась. А вот на приёме сообщений и их обработке остановимся более подробно.
Приём MIDI-сообщений
За приём любых сообщений по USART отвечает прерывание USART0_RXC
. Оттуда и посмотрим:
; USART receive byte USART0_RXC: ; Отключить прерывания cli in SrSave, SREG ; Загрузить байт из регистра in RxByte, UDR ; Записать байт в буффер ldi YL, low(notebuffer) ldi YH, high(notebuffer) ; Сместив позицию на число в счётчике clr temp add YL, ByteCnt adc YH, temp st Y, RxByte ; Проверить счётчик байтов cpi ByteCnt, 0 breq checkNoteOnOffMessage cpi ByteCnt, 2 breq handleMidiNote rjmp incCounter checkNoteOnOffMessage: ; Проверить тип сообщения и ; пропустить только NoteOn, NoteOff, CC cpi RxByte, NoteOn breq incCounter cpi RxByte, NoteOff breq incCounter cpi RxByte, ControllerChange breq incCounter DiscardByte: ; Отменить сообщение, сбросить счётчик clr ByteCnt rjmp getOutNoteCheck incCounter: inc ByteCnt getOutNoteCheck: ; Выйти из процедуры out SREG, SrSave sei reti
Что же здесь происходит? Байты, прилетающие по USART записываются в память по указателю парного регистра Y, со смещением, указанным в счётчике. Таким образом в память приходит MIDI-сообщение. Но только в том случае, если оно соответствует фильтру: пропускаются только сообщения NoteOn, NoteOff и относящиеся к изменению контроллеров через команду ControllerChange. Всё остальное откидывается, счётчик обнуляется и ожидается правильная команда. Как только счётчик указывает на поступление третьего байта, управление передаётся процедуре handleMidiNote
, о которой поговорим далее.
Обработка команд
Процедура обработки сообщений выглядит так:
handleMidiNote: ; Читаем из памяти: temp - действие, temp1 - нота memr temp, notebuffer memr temp1, notebuffer+1 mov temp2, temp1 ; Сохраним temp1 для CC memr velocity, notebuffer+2 ; Сохраним также динамику subi temp1, LowNote ; Найти правильный номер ноты inc temp1 ; Выбрать правильное действие относительно команды cpi temp, NoteOn breq doNoteOn cpi temp, NoteOff breq doNoteOff cpi temp, ControllerChange breq doControllerChange doNoteOn: ; Проверить на нулевую скорость нажатия (динамику) clr temp2 cp velocity, temp2 breq doNoteOff ; Тогда нужно NoteOff ; Далее включим ноту ; Проверяем первый голос memr temp2, voices cpi temp2, 0 brne NotFree1 ; Если свободен, ставим ноту сюда memw voices, temp1 rcall setCh1 DoneNoteOnOff: ; Всё сделано, очищаем счётчик clr ByteCnt rjmp getOutNoteCheck NotFree1: ; Проверим, свободен ли этот голос memr temp2, voices+1 cpi temp2, 0 brne DoneNoteOnOff ; Нет, нота не будет исполнена memw voices+1, temp1 rcall setCh2 rjmp DoneNoteOnOff doNoteOff: ; Проверим первый голос memr temp2, voices cp temp1, temp2 brne notOnCh1 ; Нота найдена, освобождаем первый голос clr temp1 rcall setCh1 memwi voices, 0 rjmp DoneNoteOnOff notOnCh1: ; Проверим второй голос memr temp2, voices+1 cp temp1, temp2 brne DoneNoteOnOff ; Нота нигде не найдена, выход ; Нота занимает второй голос, очищаем clr temp1 rcall setCh2 memwi voices+1, 0 rjmp DoneNoteOnOff doControllerChange: ; Проверим temp2 на "All notes off" сообщение cpi temp2, AllNotesOff brne DoneNoteOnOff ; Такое сообщение поступило, очищаем оба канала memwi voices, 0 memwi voices+1, 0 cbi DDRB, voice1 cbi DDRB, voice2 rjmp DoneNoteOnOff
Здесь вся логика действий обусловлена условными переходами. В случае включения ноты — ищется свободный канал, и если он найден, то нота записывается туда и включается воспроизведение (иначе нота игнорируется). При нулевой динамике происходит переход к обработке NoteOff, равно как и при поступлении соответствующего сообщения. Там отключаемая нота ищется сначала на первом голосе, потом и на втором, если она найдена, то отключается — иначе игнорируется. Единственное воспринимаемое CC-сообщение — All Notes Off, отключить все ноты. При его поступлении вне зависимости от содержания голосов они очищаются и воспроизведение немедленно прекращается.
Полный код программы
А вот и полный код (комментарии на английском):
ATTiny2313MIDIMusicPlayer_BlogVersion.asm
Результат
Далее можно посмотреть, как ATtiny2313 исполняет 3-ую часть «Лунной сонаты» Бетховена (для воспроизведения используется программа Guitar Pro 5):
Заключение
Надеюсь, статья была полезной! На пути следующие статьи, посвящённые MIDI и DDS (Direct Digital Synthesis). 2 канала по 4 голоса, независимо воспроизводящие синусоидальные, треугольные, пилообразные, прямоугольные сигналы при помощи этой же платы? Переделка старого синтезатора в MIDI-клавиатуру с возможностью выбора динамики? Всё это на старом знакомом ATtiny2313? Да, это есть у нас :-) Если что-то из этого особенно интересно, пишите в комментариях.
Примечания
- ↑Вообще, я пробовал оба варианта, и второй оказался довольно эффективным, хотя и не строго соответствующим спецификациям.
- ↑Вашему вниманию предлагается статья на соответствующую тему.
- ↑Обратите внимание, что USART я настраиваю на значение 38400 бод. С этим значением у драйвера Roland проблем не возникает.