Программирование подсистемы MIDI (original) (raw)
Автор: Евгений Музыченко
Источник: Компьютер Пресс
Опубликовано: 19.07.2003
Исправлено: 10.12.2016
Версия текста: 1.0
Вместе с подсистемой собственно цифрового звука (Audio, Wave) в состав звукового интерфейса Windows входит также подсистема MIDI (Musical Instrument Digital Interface - цифровой интерфейс музыкальных инструментов). Подсистема MIDI имеет два основных назначения: управление работой музыкальных синтезаторов - как реальных, так и виртуальных, реализованных в виде имитирующей программы, и цифровое представление музыкальных партитур для редактирования и обработки.
Идея MIDI состоит в унификации связи между разными электронными музыкальными инструментами (ЭМИ), которые сейчас часто называют просто синтезаторами. Каждому действию исполнителя - нажатию/отпусканию клавиши, педали, кнопки, повороту рукоятки и т.п. сопоставлен определенный тип события (event), однозначно характеризующего это действие. По каждому событию формируется сообщение (message), передаваемое по цифровому последовательному интерфейсу (порту). Каждый инструмент, получив сообщение, воспроизводит его, как будто это событие произошло при воздействии непосредственно на него.
Таким образом, исполнитель получает возможность играть сразу на нескольких инструментах, используя ограниченное число средств управления, а процесс игры может быть в точности записан и затем воспроизведен, на этих же или других инструментах. С другой стороны, MIDI–партитура может быть отредактирована, или даже создана "с нуля" путем составления из отдельных сообщений без использования клавиатуры или других органов управления.
В некотором роде принцип MIDI похож на систему макросов в современных текстовых редакторах. Вначале оператор включает запись макроса и самостоятельно выполняет все действия, которые нужно автоматизировать, а редактор запоминает их. Затем в любой момент макрос может быть воспроизведен (вызван), и тогда редактор выполняет запомненные действия в точности так же, как если бы они были заново инициированы оператором. При обнаружении ошибки в макросе многие редакторы позволяют его исправить, пользуясь специальным языком для обозначения действий. И наконец, макрос может быть написан полностью на этом языке, без непосредственного выполнения самих команд оператором.
Первоначально система MIDI использовалась только на клавишных инструментах, однако сейчас существуют MIDI–скрипки, гитары, флейты, барабаны и т.п., достаточно точно передающие основные нюансы игры исполнителя. Ведутся работы по созданию нового, более скоростного и мощного интерфейса между музыкальным инструментами.
MIDI–интерфейс содержит два уровня унификации - аппаратный, описывающий правила соединения устройств и передачи сигналов, и протокольный, описывающий протокол взаимодействия и виды передаваемых сообщений. Эта статья посвящена программированию передачи MIDI–сообщений в среде Windows, и касается аппаратной и протокольной сторон интерфейса лишь по минимуму. Полное описание того и другого можно найти в справочной литературе и различных статьях, например - в моей статье "Описание интерфейса MIDI".
Внешне подсистема MIDI очень похожа на подсистему цифрового звука Audio/Wave, и содержит - практически тот же набор функций и структур. Основная разница заключается в том, что подсистема Audio/Wave имеет дело с потоками отсчетов цифрового звука, а подсистема MIDI - с потоками сообщений.
Описание подсистемы MIDI есть во встроенной системе помощи любой современной среды программирования, а также в MSDN SDK. Microsoft поддерживает в Internet справочную систему MSDN Online, подсистема описана в разделе http://msdn.microsoft.com/library/psdk/multimed/midi_1wv3.htm.
Краткие сведения об аппаратном устройстве MIDI–интерфейса
Традиционный MIDI–интерфейс представляет собой универсальный асинхронный приемопередатчик (Universal Asynchonous Transmitter/Receiver, UART), похожий на обычный последовательный порт компьютера, и так же ориентированный на обмен байтами. Разница заключается в том, что в MIDI жестко фиксирована скорость обмена данными - 31250 бит/сек, и для передачи сигналов используется принцип "токовая петля" в отличие от передачи напряжением, как в последовательных портах на IBM PC. Разъемы входа (In) и выхода (Out) разделены, и представляют собой круглые 5–контактные розетки типа DIN-5, аналогичные разъемам отечественной звуковой аппаратуры 70-80-х годов.
Для соединения устройств используется симметричный кабель с одинаковыми разъемами-вилками на концах. Принцип токовой петли и низкая скорость обмена позволяет значительно увеличить длину соединительного кабеля - в стандарте MIDI она может достигать 15 метров, хотя на практике работают кабели со значительно большей длиной. В MIDI используется каскадный способ соединения устройств: один кабель может соединять только два устройства, но каждое устройство должно иметь специальный выход ретранслятора (Thru), на который копируются все данные, полученные устройством на входе. Таким образом, несколько устройств могут быть соединены в цепочку, либо более сложным способом, образуя MIDI–сеть.
Описанная разница со схемой последовательного порта IBM PC не позволяет подключать MIDI–устройства к компьютеру через этот порт. Даже если воспользоваться схемой преобразования сигналов, скорость передачи 31250 бит/с в стандартном порту получить невозможно, кроме отдельных интерфейсных микросхем, специально разработанных для совместимости с MIDI. Большинство типовых звуковых карт содержит в себе MIDI–интерфейс, однако сигналы в нем передаются в стандарте ТТЛ, а не токовой петли, и вдобавок используются разные типы разъемов. Поэтому для подключения MIDI–устройств к компьютеру необходим специальный адаптер - либо в виде самостоятельной интерфейсной платы, либо в виде переходника от разъема Joystick/MIDI типа DB-15 к разъемам MIDI In/Out/Thru типа DIN-5.
Со стороны Windows–приложения любой MIDI–интерфейс видится, как пара системных MIDI–портов - ввода (In) и вывода (Out). Приход сообщения на вход интерфейса приводит к их появлению в системном порту In, откуда их считывает приложение, а передача приложением сообщений в системный порт Out приводит к их выдаче на выход интерфейса. Однако поведение драйвера в Windows таково, что в случае коротких (1-3 байта) сообщений обмен ведется только полными сообщениями, а не отдельными байтами. Иначе говоря, приложение не получит уведомления до тех пор, пока на вход интерфейса не поступят все составляющие сообщение байты.
Основные черты и понятия интерфейса и подсистемы MIDI
Партитура и звук
Очень важным свойством MIDI–протокола является то, что в его сообщениях ни в каком виде не содержится готового звука. Содержится только информация о том, когда и какие ноты должны звучать, какими тембрами и с какими параметрами они должны быть исполнены, и т.п. Непосредственно звук создается только на выходе синтезатора, и очень сильно зависит его от типа, структуры, настройки и прочих факторов. Если поток цифрового звука полностью определяет звучание, то поток MIDI–сообщений определяет лишь основную схему исполнения музыкального произведения. Точное соответствие звучания возможно лишь на полностью совместимых синтезаторах, что в профессиональной области встречается лишь внутри линий одного производителя. Одну и ту же партитуру простейший FM–синтезатор звуковой карты может исполнить "брякающе", несложный таблично-волновой - "плоско", "невыразительно", а профессиональный или просто подходящий - "объемно", "живо", "выпукло" и т.п.
Каналы и тембры
Поскольку музыкальная партитура обычно состоит из нескольких независимых партий различных инструментов, в MIDI–протоколе предусмотрено разделение основных сообщений по виртуальным каналам (channels). Одно MIDI–устройство может поддерживать до 16 каналов (1..16), в каждом из которых может быть установлен собственный тембр звучания (часто называемый инструментом (instrument) или патчем (patch)).
Сообщения о начале/конце звучания ноты, смене тембра и параметров звучания имеют в своем составе номер канала, к которому они относятся; такие сообщения называются канальными. Остальные сообщения, предназначенные инструменту в целом, называются системными.
Каждый канал может работать в двух режимах: мелодическом (melodic) и ударном (percussion). В первом случае номер клавиши, передаваемый в сообщениях, определяет номер хроматического полутона ("До" первой октавы имеет номер 60). Во втором случае номер клавиши определяет произвольный (обычно ударный) звук фиксированной высоты; можно сказать, что по клавиатуре "раскладывается" множество различных звуков, каждый из которых извлекается определенной клавишей.
Канал 10 по умолчанию обычно находится в ударном режиме, остальные каналы - в мелодическом.
Номера и звучание тембров
Существуют различные стандарты на соответствие условных номеров тембров их звучанию. Минимальный стандарт - General MIDI (GM) - определяет лишь базовые свойства 128 мелодических и 37 ударных тембров, однако реализации стандарта очень сильно различаются. Стандарт GS задает более жесткие требования на соответствие номеров и звучаний, а стандарт XG обеспечивает практически полное соответствие. На данный момент XG является единственным мировым стандартом, обеспечивающим практически абсолютно точное воспроизведение партитур на различных моделях синтезаторов.
Кэширование загружаемых тембров
На заре развития звуковых адаптеров с таблично-волновыми синтезаторами остро стояла проблема минимизации объема оперативной памяти, используемой для загрузки тембров (patches). Тогда был предложен способ кэширования - в память синтезатора загружался лишь тот набор тембров, который использовался в очередной композиции, а при переходе к следующей композиции содержимое кэша обновлялось - частично или целиком. Этот способ не получил распространения, и единственным известным адаптером, который его использует, остался Gravis Ultrasound (GUS). Тем не менее, в подсистеме MIDI была реализована поддержка кэширования, и приложения должны использовать этот механизм, если планируется работа с синтезаторами GUS.
Реальные и виртуальные синтезаторы
Музыкальный инструмент, управляемый по MIDI–протоколу, может быть как аппаратным (реальным), сделанным в виде физического устройства, либо программным (виртуальным), выполненным в виде специального драйвера. Реальный синтезатор воспринимает MIDI–сообщения, переданные по физическим линиям связи, отрабатывает их и преобразует в звук на физическом выходе. Виртуальный синтезатор воспринимает сообщения, переданные его виртуальному системному MIDI–порту, создает на их основе поток цифрового звука, который выводится через обычный звуковой адаптер (Audio/Wave), либо записывается непосредственно на диск (файл типа Wav).
Виртуальные синтезаторы могут имитировать работу реальных (Roland VSC, Yamaha S-YG, S-YXG), либо воплощать новую, физически не существующую модель синтезатора (Reality, GigaSampler, Generator и т.п.). Большинство виртуальных синтезаторов имеет только входной порт (видимый со стороны приложения, как выходной, Out), поскольку они не имеют клавиатуры и не могут сами генерировать сообщения.
Внешние и внутренние синтезаторы
Реальные MIDI–синтезаторы могут быть внешними - классические клавишные инструменты или блоки-тонгенераторы, и внутренними - типовые интерфейсные карты или их дочерние платы. Синтезатор, реализованный в виде интерфейсной карты, не имеет классического последовательного MIDI–интерфейса, так как вся информация передается по компьютерной магистрали. Существующие дочерние платы подключаются через специальный внутренний последовательный интерфейс.
Синхронные и асинхронные драйверы
Типовой MIDI–порт является асинхронным и содержит внутреннюю очередь, так что операции вывода сообщений в порт завершаются немедленно, а фактический вывод байтов через физический интерфейс происходит параллельно и независимо. Драйвер такого порта возвращает управление сразу же после записи байтов в очередь (короткие сообщения), либо после включения в собственную очередь буфера данных длинного сообщения.
Многие внутренние и все виртуальные синтезаторы не содержат физического порта, и являются синхронными - каждое полученное сообщение отрабатывается сразу и полностью. Драйвер такого синтезатора возвращает управление лишь после того, как полученное сообщение будет полностью обработано, что может потребовать большего времени - до единиц-десятков миллисекунд.
Управление громкостью
Интерфейс подсистемы MIDI включает средства управления общей громкостью (main volume) на звуковом выходе синтезатора. Однако функциональность этих средств сильно ограничена вследствие того, что в стандарте MIDI нет единого способа управления общим уровнем громкости на выходе. Поэтому управление громкостью обычно доступно только для виртуальных и внутренних синтезаторов, имеющих собственные специализированные драйверы, которые "знают", как управляется громкость на этой модели синтезатора. Некоторые внешние синтезаторы тоже имеют специализированные драйверы, в которых может быть доступно и управление громкостью.
Сходство подсистем MIDI и Audio/Wave
Подсистемы MIDI и Audio/Wave очень схожи. Обе являются программными интерфейсами с устройствами ввода/вывода (In/Out) и содержат примерно одинаковый набор функций. Многие функции двух подсистем различаются только префиксами (midi/wave), в остальном же логика их работы полностью совпадает. Подсистема MIDI точно так же поддерживает асинхронный обмен данными с различными видами уведомлений; схемы взаимодействия приложения с драйвером при этом почти в точности совпадают.
Как и подсистема Audio, подсистема MIDI в Windows 95/98 является 16-разрядной и наследуется непосредственно из Windows 3.x. В Windows NT/2000 все подсистемы являются 32-разрядными.
Описание программного интерфейса с базовой звуковой подсистемой читайте в статье "Низкоуровневое программирование звука в Windows", опубликованной в CD-приложении к "Компьютер Пресс" N 6 за 2000 г.
Структура MIDI–сообщений
MIDI–сообщения делятся на короткие (1-3 байта) и длинные (более трех байтов). Длинные представляют собой особый вид сообщений - системные исключительные (System Exclusive, или SysEx), которые имеют переменную длину.
Поскольку MIDI–протокол создавался для связи между устройствами, которые могут быть соединены и разъединены в любой момент, структура сообщений сделана самосинхронизирующейся, чтобы приемное устройство всегда могло выделить начало сообщения. Первый байт каждого сообщения, называемый также байтом состояния (status byte) имеет установленный (единица) старший бит, во всех остальных байтах (байты данных, data bytes) старший бит сброшен (нуль).
Байты состояния канальных сообщений содержат в младшей тетраде номер канала (нумерация с нуля).
Сообщения типа SysEx имеют байт состояния F0. Для индикации конца SysEx используется байт F7 (End Of Exclusive - EOX). Все байты данных расположенные между ними, считаются принадлежащими этому сообщению, и имеют нулевой старший бит.
Режим Running Status
Когда в потоке одно за другим следуют канальные сообщения одного типа, относящиеся к одному каналу, их байты состояния совпадают. В этом случае для сокращения плотности потока может использоваться режим, называемый Running Status, при котором повторяющиеся байты состояния не передаются. Приемное устройство, получив байт состояния канального сообщения, должно запомнить его. Если после приема всех байтов данных сообщения очередной байт не является байтом состояния (старший бит не установлен) - это означает, что подразумевается точно такой же байт состояния, а принятый байт является первым байтом данных сообщения.
Например, сообщения о нажатии/отпускании клавиш в канале 2 имеют байт состояния 92 и содержат два байта данных. Пара таких сообщений кодируется, как 92 3C FF, 92 3C 00, а при использовании Running Status второй байт 92 не передается. При смене типа сообщения или канала вновь передается один байт состояния, после чего идет серия байтов данных.
При передаче сообщений SysEx режим не используется; любое сообщение SysEx отменяет запомненный байт состояния, и первое канальное сообщение после SysEx обязательно должно содержать байт состояния.
Когда подсистема MIDI передает приложению принятое короткое сообщение, оно всегда приводится драйвером к каноническому виду. Первый байт принятого сообщения всегда является байтом состояния, независимо от того, в каком режиме оно было получено MIDI–портом.
Отличие потока MIDI–данных от потока цифрового звука
Поток отсчетов цифрового звука имеет однородную структуру, определяемую форматом, и постоянную скорость движения данных - отсчеты следуют друг за другом непрерывно с заданной частотой дискретизации. MIDI–поток состоит из разнородных сообщений, каждое из которых определяет какое-либо событие. Поток сообщений обычно неравномерен, поскольку большинство событий привязано к определенным моментам времени.
Скорость MIDI–потока намного (полтора порядка) ниже, чем потока цифрового звука, а большинство сообщений имеет длину 1-3 байта. Это позволяет передавать и принимать сообщения по одному, а не упаковывать их в блоки, как это делается в подсистеме цифрового звука. Однако сообщения SysEx, длина которых может достигать нескольких десятков килобайт, всегда передаются и принимаются в виде блоков.
Временная шкала и темп потока вывода
Поскольку каждое MIDI–сообщение привязано к определенному моменту времени, а поток сообщений неоднороден, требуется опорная временн_а_я сетка, относительно которой ведется отсчет времени. В MIDI для этого используется понятие тика (tick) - минимального кванта времени, на который могут отстоять друг от друга MIDI–события. Длительность тика определяется путем деления либо длительности четвертной доли (quarter note), либо независимого временного интервала. В первом случае получается относительное деление, обычно используемое в музыке, во втором - абсолютное, используемое при синхронизации с изображением. На практике используются значения от 48 до 480 тиков на четвертную долю, наиболее часто - 96 и 120.
В соответствии со спецификацией формата стандартного MIDI–файла (SMF) 1.0, параметры длительности тика задаются 16-разрядным словом (тип short int). Положительные значения задают количество тиков в четвертной доле, отрицательные - непосредственно частоту тиков в формате SMPTE (Society of Motion Picture and Television Engineers - сообщество инженеров кино и телевидения). Отрицательное значение формируется из двух байтов по формуле MAKEWORD (TicksPerFrame, -FramesPerSec), где TicksPerFrame - количество тиков в кадре, FramesPerSec - количество кадров в секунде (обычно 24, 25, 29 или 30). Отрицательное значение старшего байта дает отрицательное значение всего слова.
Если задано относительное деление, то частота тиков в секунду рассчитывается, исходя из темпа и количества тиков в четвертной доле. Темп потока вывода задается длительностью четвертной доли в микросекундах. Например, для размера 4/4 темп 120 представляется значением 500 000.
Непосредственный и буферизованный ввод/вывод сообщений
В отличие от подсистемы Audio, где для ввода и вывода цифрового звука применяется один и тот же потоковый метод с очередью буферов, в подсистеме MIDI ввод и вывод различных типов данных выполняется разными способами.
Ввод/вывод коротких MIDI–сообщений происходит непосредственно, без какой-либо специальной подготовки. О приходе каждого очередного короткого MIDI–сообщения приложение уведомляется посылкой системного сообщения или вызовом функции, в параметрах которых и передается полученное MIDI–сообщение в упакованном виде. Вывод коротких сообщений также выполняется при помощи простого вызова функции ShortMsg, одним из параметров которой является упакованное тем же методом MIDI–сообщение.
Длинные сообщения принимаются и выводятся при помощи очереди буферов, передаваемых драйверу, как и в подсистеме Audio. Если к моменту прихода длинного сообщения в очереди драйвера нет ни одного буфера - сообщение отвергается. При приеме завершающего байта сообщения (F7)
Автоматизированный потоковый вывод в подсистеме MIDI
Как и при использовании базовой звуковой подсистемы, в подсистеме MIDI возможен потоковый вывод, когда драйверу передается серия блоков-буферов, в каждом из которых находится несколько сообщений. Однако сам поток, в связи с его неоднородностью, оформляется при этом специальным образом. Поток MIDI–вывода представляет собой последовательность событий, каждое из которых описывает либо MIDI–сообщение, либо одно из специальных мета-событий - смену темпа, комментарий и т.п. Подсистема MIDI последовательно перебирает события в потоке, выполняя для каждого из них соответствующее действие; для MIDI–сообщений таким действием является его передача в выходной порт.
Каждое событие в потоке вывода представляется специальным описателем, содержащим вместе с кодом события и другими параметрами значение интервала времени (Delta Time), чтобы можно было определить, в какой момент его нужно обработать.
В отношении потока вывода применяются те же операции, что и для потока цифрового звука - запуск, остановка, возобновление, запрос позиции, сброс.
Надо заметить, что поток вывода в подсистеме MIDI - структура более высокого уровня, нежели собственно поток MIDI–сообщений. Поток вывода состоит из мета–событий, из которых обычно большинство, но не все, представляют собой MIDI–сообщения.
Подготовка буферов данных
Подготовка (фиксация в памяти) буферов данных перед их передачей подсистеме требуется только для устройств ввода и вывода, так как только они обслуживаются низкоуровневым драйвером, который обычно работает с аппаратными прерываниями и DMA. Потоки вывода обслуживаются подсистемой на более высоком уровне секвенсора MCI, который обменивается с буферами данных только в режиме приложения (user mode). Поэтому для буферов данных потока вывода не требуется какой-либо подготовки перед их передачей функции вывода.
Зависание нот
В структуре MIDI–протокола сообщения о начале (нажатии) и окончании (отпускании) звучания нот являются полностью независимыми, и каждая нота оформляется парой сообщений о нажатии/отпускании. При переполнении буфера приемного устройства, ошибках передачи, временном разрыве связи некоторые сообщения могут искажаться и пропадать; в их числе могут оказаться и сообщения об отпускании нот. В этом случае синтезатор, получивший ранее сообщения о нажатии этих нот, продолжает генерировать соответствующие звучания до тех пор, пока не будет выключен, не получит сообщения об отпускании каждой из нажатых нот, либо не получит в соответствующем канале сообщения "All Notes Off" (отключить все ноты). Это трехбайтовое канальное сообщение, которое кодируется, как Bn 7B 00, где n - номер канала.
Внутренний синтезатор, непосредственно управляемый драйвером, отрабатывает сброс нот при выполнении приложением функции сброса устройства. Однако внешний синтезатор не может узнать о том, что эта функция была вызвана, поэтому ему требуется передать это сообщение явно.
Для предотвращения зависания нот рекомендуется вызывать функцию сброса и посылать во все каналы сообщения "All Notes Off" каждый раз перед закрыванием устройства или потока вывода.
Программные события и уведомление о них
Если в базовой звуковой подсистеме существует только одно асинхронное (возникающее не в момент выполнения интерфейсной функции) событие, которое сообщает о завершении обработки очередного буфера, то в подсистеме MIDI их четыре: прием короткого сообщения, прием длинного сообщения, завершение вывода буфера, и достижение заданной позиции в потоке вывода. О наступлении каждого из этих событий подсистема MIDI может уведомлять приложение теми же способами, что и звуковая подсистема.
Дуплекс
В отличие от адаптеров цифрового звука, реальные MIDI–интерфейсы всегда работают в полном дуплексе, даже если они входят в состав полудуплексного звукового адаптера. Обработка цифрового звука и MIDI–данных в адаптере всегда реализуется независимо, и для MIDI–данных всегда предусматривается внутренний буфер хотя бы на один-два байта, что позволяет вводить и выводить MIDI–данные одновременно и независимо.
Поддержка нескольких клиентов
Подсистема MIDI допускает работу с устройством нескольких процессов (клиентов) одновременно, однако практически все существующие драйверы являются одноклиентными, не позволяя открывать MIDI–порт нескольким приложениям одновременно. Точнее говоря - для уже открытого устройства драйвер отвергает все последующие запросы открывания, даже если они исходят от того же самого процесса.
Такое поведение драйвера очень неудобно при использовании программных панелей управления синтезатором совместно с музыкальным редактором (секвенсором). Существуют дополнительные MIDI–драйверы, реализующие поддержку нескольких клиентов для любых портов - MultiMid и Hubi's Loopback. После установки любого из них для всех установленных MIDI–портов создаются многоклиентные копии.
Во время воспроизведения потока вывода допускается параллельный вывод на то же самое устройство коротких сообщений, которые в этом случае корректно включаются подсистемой в общую структуру потока MIDI–сообщений, адресованных устройству. Таким образом может быть реализована, например, игра с сопровождением, когда поток вывода воспроизводит аккомпанемент, а исполнитель играет мелодическую партию, передаваемую отдельными короткими сообщениями параллельно с потоком.
Короткие сообщения, посылаемые параллельно с потоком вывода, адресуются непосредственно потоку (в функции ShortMsg задается ключ потока). Сообщения должны быть полностью сформированными - режим Running Status в этом случае запрещен.
Служба переназначения устройств, каналов, тембров и клавиш
Для MIDI–портов, как и для звуковых, Windows содержит службу переназначения - MIDI Mapper. Существует понятие стандартного системного устройства ввода и стандартного системного устройства вывода, которые задаются в закладке MIDI формы свойств мультимедиа. Приложение может запросить работу либо с конкретным устройством, либо со стандартным системным - в последнем случае нужное устройство определяет служба переназначения.
Кроме этого, MIDI Mapper может переназначать номера каналов, тембров, а также клавиш в каналах ударных, в соответствии с заданной схемой переназначения. Это может помочь при переносе партитур на другие модели синтезаторов, имеющие иное назначение каналов, раскладку тембров и ударных звуков по номерам. Для каждого MIDI–устройства может быть независимо задана своя схема переназначения.
И наконец, MIDI Mapper может масштабировать параметры сообщений громкости в канале (Main Volume), проходящих через него. Значения параметров умножаются на заданный коэффициент, и в результате общая громкость произведения повышается или понижается. Однако такой способ нельзя назвать удачным - он введен лишь в качестве крайней меры.
Номера MIDI–устройств
Подсистема нумерует установленные устройства, начиная с нуля. При установке нового устройства или удалении существующего нумерация изменяется, поэтому даже во время работы программы в системе могут появиться или исчезнуть MIDI–устройства.
Вместо номера устройства может использоваться ключ (handle) ранее открытого устройства; система автоматически определяет, какое именно значение передано интерфейсной функции.
Идентификаторы (ключи) открытых устройств
При открывании объекта - устройства или потока - подсистема возвращает его идентификатор, или ключ (handle), по которому затем происходит вся остальная работа с объектом. Формально идентификаторы устройств ввода и вывода имеют различные типы - HMIDIIN и HMIDIOUT, однако оба они эквивалентны (если не определена макропеременная STRICT) типу HMIDI, который может использоваться для создания универсальных функций, не зависящих от типа устройства. Ключ потока вывода имеет тип HMIDISTRM, и в некоторых функциях может использоваться вместо ключа устройства вывода; в таких случаях требуется явное приведение к типу HMIDIOUT.
Имена интерфейсных функций
Подсистема MIDI, в отличие от звуковой подсистемы, предоставляет приложению три, а не два, класса объектов: устройство ввода (In), устройство вывода (Out) и буферизованный поток (Stream). Соответственно, имеется три класса интерфейсных функций для обслуживания этих объектов. Имена функций имеют соответствующие префиксы - midiInStart, midiStreamOut и т.п. Чаще всего одноименные функции с разными префиксами различаются только видами объектов, к которым они относятся; в таком случае я буду описывать все функции в одной главе и ссылаться на них по общей части имени, вместо типа объекта в имени будет писаться "xxx". Если же смысл и поведение функций различается - они будут описываться и упоминаться по отдельности.
Общая схема взаимодействия программы и подсистемы MIDI
Если программе безразлично, с каким конкретно устройством она будет работать, либо работа ведется только со стандартным системным устройством, программа может ориентироваться только на службу переназначения. В противном случае программа определяет количество имеющихся в системе устройств ввода и/или вывода при помощи функций GetNumDevs.
При необходимости программа может запросить параметры и имена MIDI–устройств при помощи функций GetDevCaps.
Работа программы с устройством ввода начинается с его открывания функцией Open. При этом программа указывает требуемый способ уведомления о наступлении различных событий.
Для приема коротких сообщений программе достаточно предусмотреть обработчик асинхронных событий типа DATA, который будет получать уведомление от подсистемы MIDI каждый раз, когда на вход интерфейса поступит очередное MIDI–сообщение. Поскольку длина коротких сообщений не превышает 3 байтов, они передаются обработчику в числе параметров функции или сообщения упакованными в переменную типа DWORD, и для их приема не требуется выделения каких-либо буферов в памяти. Прием сообщений начинается сразу же после обращения к функции Start.
Для вывода коротких сообщений применяется функция ShortMsg, в которой сообщение передается целиком, упакованным в переменную типа DWORD.
Для приема и вывода длинных сообщений необходимо использовать механизм буферизации - по той же схеме, что и для записи/воспроизведения цифрового звука. Для этой цели служат функции Prepare/Unprepare, AddBuffer, LongMsg, Stop, Reset.
Для устройств вывода, поддерживающих расширенные функции управления, программа может регулировать громкость звука функцией SetVolume.
Для автоматизированного потокового вывода функцией midiStreamOpen создается MIDI–поток, привязанный к заданному устройству вывода. Затем приложение формирует последовательность мета-событий, оформляя их в виде цепочки описателей событий, последовательно расположенных в памяти. Последовательность может занимать один непрерывный блок памяти, либо быть разбита на несколько блоков. Сформированные блоки последовательно передаются подсистеме функцией midiStreamOut.
Перед запуском воспроизведения потока для него могут быть установлены параметры темпа и временной шкалы функцией midiStreamProperty. Темп потока может быть изменен этой же функцией в любое время.
Воспроизведение потока может быть приостановлено функцией midiStreamPause, после чего возобновлено функцией midiStreamRestart. Для аварийного прерывания обработки потока используется функция midiStreamReset, немедленно останавливающая процесс ввода или вывода и возвращающая все буферы из очереди приложению.
При полном завершении работы с устройством или потоком они закрываются функцией Close.
Средства разработки, включаемые файлы и библиотеки
Как всегда, описывается программирование на языке C/C++ в среде Microsoft Visual C++.
Все необходимые константы, типы, структуры и прототипы функций подсистемы определяется в файле MMSYSTEM.H, который по умолчанию включается в компиляцию из общего файла WINDOWS.H. Дополнительные, редко используемые константы определены в файле MMREG.H.
Интерфейсные функции импортируются из библиотеки WINMM.LIB.
Структуры, используемые в интерфейсе
При передаче указателей на некоторые структуры в параметрах интерфейсных функций передаются также размеры этих структур. Это делается для того, чтобы подсистема могла отслеживать версию интерфейса, используемую программой - 16- или 32-разрядный, ASCII или UNICODE.
Структуры MIDIINCAPS и MIDIOUTCAPS
Описывают свойства и характеристики MIDI–устройства. Все поля структур заполняются только подсистемой и драйвером MIDI. Структуры MIDIINCAPS и MIDIOUTCAPS весьма похожи и имеют четыре общих поля:
WORD wMid; WORD wPid; MMVERSION vDriverVersion; CHAR szPname [MAXPNAMELEN];
Индивидуальная часть структуры MIDIINCAPS имеет одно дополнительное поле:
Индивидуальная часть структуры MIDIOUTCAPS имеет пять дополнительных полей:
WORD wTechnology; WORD wVoices; WORD wNotes; WORD wChannelMask; DWORD dwSupport;
- wMid, wPid - идентификаторы разработчика (Manufacturer) и самого драйвера (Product). Полный список известных на момент выпуска SDK идентификаторов определен в файле MMREG.H
- vDriverVersion - версия драйвера, представленная младшим словом (тип MMVERSION эквивалентен типу UINT, который в Win32 является 32-разрядным). Старший байт слова представляет основной номер версии, а младший байт - номер подверсии, отражающей непринципиальные изменения драйвера. Для выделения каждого номера можно использовать стандартные макросы HIBYTE и LOBYTE.
- szPname - имя устройства в виде строки ASCIIZ.
- dwSupport - описатель поддерживаемых драйвером синтезатора или порта возможностей и режимов. Для устройств ввода (структура MIDIINCAPS) это слово не имеет смысла и всегда равно нулю; для устройств вывода (структура MIDIOUTCAPS) оно может содержать ряд флагов, имена которых имеют префикс MIDICAPS_:
VOLUME | Поддержка управления громкостью. |
---|---|
LRVOLUME | Поддержка независимого по каналам управления громкостью. |
STREAM | Прямая поддержка драйвером потоков вывода. |
CACHE | Поддержка кэширования тембров. |
- wTechnology - тип устройства. Константы типов имеют префикс MOD_:
MIDIPORT | Аппаратный MIDI–порт. |
---|---|
MAPPER | Порт службы переназначения. |
SYNTH | Внутренний синтезатор с технологией неизвестного типа. |
FMSYNTH | Внутренний синтезатор с частотно-модуляционной технологией. |
SQSYNTH | Внутренний синтезатор выходными сигналами в форме меандра (square wave). |
Внутренние таблично-волновые синтезаторы имеют тип MOD_SYNTH.
- wVoices - максимальное количество одновременно звучащих голосов (одна нота может одновременно звучать несколькими голосами). Имеет смысл только для внутренних синтезаторов, иначе равно нулю.
- wNotes - максимальное количество одновременно активных нот. Имеет смысл только для внутренних синтезаторов, иначе равно нулю.
- wChannelMask - маска поддерживаемых внутренним синтезатором MIDI–каналов (каналу 1 соответствует бит 0). Для внешних портов всегда равно 0xFFFF.
Структура MIDIHDR
Называется заголовком буфера и описывает буфер - как хранилище данных и как элемент очереди драйвера. Изначально заполняется приложением, впоследствии отдельные поля модифицируются драйвером.
LPSTR lpData; DWORD dwBufferLength; DWORD dwBytesRecorded; DWORD dwUser; DWORD dwFlags; LPMIDIHDR lpNext; DWORD reserved; DWORD dwOffset; DWORD dwReserved [4];
- lpData - указатель буфера данных (тип char *). Устанавливается приложением, драйвером не изменяется.
- dwBufferLength - размер буфера в байтах. Устанавливается приложением, драйвером не изменяется.
- dwBytesRecorded - для буферов ввода длинных сообщений обозначает количество байтов, записанных устройством в буфер, устанавливается драйвером и имеет значение только после завершения обработки данного буфера. Для буферов потоков вывода устанавливается приложением, и обозначает количество байтов, реально занятое описателями сообщений.
- dwUser - поле пользователя, в которое программа может занести любое угодное ей значение - например, ссылку на внутренний описатель устройства. Устанавливается приложением, драйвером не используется и не изменяется.
- dwFlags - слово состояния буфера. Устанавливается приложением, модифицируется драйвером. Имена констант флагов состояния имеют префикс MHDR_:
PREPARED | Буфер подготовлен (зафиксирован в памяти) |
---|---|
INQUEUE | Буфер находится в очереди драйвера |
DONE | Обработка буфера драйвером завершена |
ISSTRM | Буфер принадлежит потоку вывода |
- lpNext - указатель заголовка следующего в очереди буфера. Управляется только драйвером.
- reserved - служебное поле. Управляется только драйвером.
- dwOffset - смещение в буфере описателя мета-события уведомления о достижении позиции (в оригинале это событие называется Callback Event). Устанавливается драйвером непосредственно перед выполнением уведомления. Если уведомление обрабатывается недостаточно быстро, и в потоке встречается следующее мета-событие - драйвер перезапишет смещение в этом поле, выполняя уведомление для следующего мета-события.
- dwReserved - служебное поле. Управляется только драйвером.
Структура упакованного короткого сообщения
Короткие сообщения передаются упакованными в переменные типа DWORD. По сути, сообщение просто записывается в область памяти, отведенную под эту переменную, начиная с первого (младшего) байта. Таким образом, первый байт сообщения (это байт состояния, если не используется Running Status) попадает в младший байт переменной, а следующие байты сообщения - в следующие по старшинству байты переменной. Поскольку длина коротких сообщений не превышает трех байтов, старший байт переменной сообщением никогда не используется.
По отдельности байты сообщения из упакованной переменной можно извлекать, поочередно сдвигая ее значение вправо на 8 разрядов, либо приведя к типу указателя на байт (char * или BYTE *) , и последовательно выбирая байты в порядке возрастания адресов.
Структура MIDIEVENT
Описывает мета-событие в потоке вывода. Заполняется приложением.
DWORD dwDeltaTime; DWORD dwStreamID; DWORD dwEvent; DWORD dwParms [];
- dwDeltaTime - интервал времени от предыдущего события, в тиках.
- dwStreamID - зарезервированное поле, должно иметь нулевое значение.
- dwEvent - код типа события и дополнительные флаги (старший байт), параметры события (остальные байты). Для выделения кода и параметров служат макросы MEVT_EVENTTYPE и MEVT_EVENTPARM. Имена констант кодов событий и дополнительных флагов имеют префикс MEVT_.
Флаги, указываемые вместе с кодом события:
F_SHORT | Событие с коротким представлением. Все параметры события находятся в младших 24 разрядах слова, выделяемых макросом MEVT_EVENTPARM. |
---|---|
F_LONG | Событие с длинным представлением. Блок параметров события располагается, начиная с поля dwParms, длина блока определяется младшими 24 разрядами слова. |
F_CALLBACK | При достижении этого события выполняется уведомление о достижении позиции. Уведомление может быть запрошено для событий любого типа. |
Один из флагов длины представления события должен быть задан обязательно.
Коды типов событий:
SHORTMSG | Короткое MIDI–сообщение (короткое представление). Байты сообщения упакованы в поле параметров события. |
---|---|
LONGMSG | Длинное MIDI–сообщение (длинное представление), предназначено исключительно для передачи сообщений SysEx. Полностью сформированное сообщение SysEx находится в блоке параметров события. Передача с помощью этого события серии коротких сообщений не рекомендуется, так как при этом не отрабатывается режим Running Status. |
TEMPO | Изменение темпа (короткое представление). Значение темпа представлено в поле параметров события, в виде длительности четвертной доли в микросекундах. Событие смены темпа не отрабатывается, если шкала времени для потока задана в формате SMPTE. |
COMMENT | Комментарий (длинное представление). Первый байт блока параметров кодирует тип комментария, остальные байты - сам комментарий. Событие не влечет никаких специальных действий, блок параметров игнорируется. |
VERSION | Информация о версии (длинное представление). Блок данных содержит структуру типа MIDISTRMBUFFVER. |
NOP | Пустое событие (короткое представление). Поле параметров игнорируется. |
- dwParms - Первое двойное слово блока параметров, который должен содержать также целое количество двойных слов, и при необходимости дополняться нулевыми байтами до границы двойного слова. В младшей части поля dwEvent всегда указывается реальная длина блока параметров в байтах.
Структура MMTIME
Описывает время в терминах различных мультимедийных потоков. Используется для получения текущей позиции потока вывода. Приложением устанавливается только поле wType, остальные поля заполняются драйвером.
UINT wType; union { DWORD ms; DWORD sample; DWORD cb; DWORD ticks; struct { BYTE hour; BYTE min; BYTE sec; BYTE frame; BYTE fps; BYTE dummy; BYTE pad[2] } smpte; struct { DWORD songptrpos; } midi; } u;
- wType - формат времени, описываемого структурой. Константы формата имеют префикс TIME_:
BYTES | Количество байтов от начала потока |
---|---|
MIDI | Время в стандарте MIDI (Sond Position Pointer) |
MS | Время в миллисекундах |
SAMPLES | Количество звуковых блоков с начала звукового (Audio) потока |
SMPTE | Время в стандарте SMPTE (в кадрах) |
TICKS | Время в тиках от начала MIDI–потока |
- ms - счетчик миллисекунд, для случая TIME_MS.
- sample - счетчик звуковых блоков, для случая TIME_SAMPLES.
- cb - счетчик байтов, для случая TIME_BYTES.
- ticks - счетчик тиков, для случая TIME_TICKS.
- smpte - набор данных в формате SMPTE, для случая TIME_SMPTE.
- midi - данные в формате MIDI–времени.
- hour, min, sec - счетчик полных часов, минут и секунд.
- frame - счетчик кадров внутри последней секунды.
- fps - кадров в секунду (24, 25, 29, 30).
- dummy, pad - выравнивание на границу двойного слова.
- songptrpos - позиция указателя композиции в MIDI (Song Position Pointer).
Структуры MIDIPROPTEMPO и MIDIPROPTIMEDIV
Описывают свойства потока вывода - темп и временн_у_ю шкалу. Формат структуры MIDIPROPTEMPO:
DWORD cbStruct; DWORD dwTempo;
- cbStruct - размер структуры в байтах, отражает версию интерфейса. В будущем возможно добавление новых полей.
- dwTempo - темп воспроизведения потока, в микросекундах на четвертную долю.
Формат структуры MIDIPROPTIMEDIV:
DWORD cbStruct; DWORD dwTimeDiv;
- cbStruct - размер структуры в байтах.
- dwTimeDiv - длительность деления временной шкалы (тика) потока (младшее слово).
Структура MIDISTRMBUFFVER
Описывает версию потока вывода.
DWORD dwVersion; DWORD dwMid; DWORD dwOEMVersion;
- dwVersion - версия потока (старшее/младшее слово содержат старшую/младшую часть номера версии). В данное время поддерживается только версия 1.0.
- dwMid - идентификатор производителя. Константы для кодов идентификаторов определены в файле MMREG.H.
- dwOEMVersion - номер версии производителя, создающего поток.
Уведомления, передаваемые программе подсистемой MIDI
Для сообщения программе о наступлении различных событий в отношении устройств и потоков подсистема MIDI может использовать три вида уведомлений: установку объекта программного события (event), вызов заданной программной функции (callback), либо посылку сообщения заданному окну (window) или задаче (thread). В первом варианте программа получает информацию лишь о самом факте некоторого события, и сама должна выяснять, что именно произошло; во втором и третьем вариантах передается код события и уточняющая информация (параметры события).
Уведомление при помощи программного события возможно только в отношении объектов вывода (выводной порт или поток вывода).
Типы событий, о которых сообщается приложению
В подсистеме MIDI возможно возникновение шести различных событий. В скобках приведены общие части имен констант, кодирующих виды событий. Имена параметров указаны традиционные - параметры wParam, lParam относятся к сообщеням Windows, а Param1, Param2 - к вызовам callback–функций:
- Открывание устройства или потока (OPEN). В параметре wParam передается ключ объекта - устройства или потока.
- Закрывание устройства или потока (CLOSE). В параметре wParam передается ключ объекта.
- Приход короткого сообщения (DATA/MOREDATA/ERROR). В параметре lParam или Param1 передается упакованное короткое MIDI–сообщение - полное и достоверное для событий DATA/MOREDATA, и неполное или ошибочное - для события ERROR. Функции в параметре Param2 дополнительно передается время прихода сообщения относительно запуска приема (timestamp).
- Приход длинного сообщения (LONGDATA/LONGERROR). В параметре lParam или Param1 передается указатель заголовка буфера данных, содержащего либо достоверное, либо незавершенное или ошибочное сообщение. Функции в параметре Param2 дополнительно передается время прихода сообщения.
- Завершение вывода длинного сообщения или буфера потока (DONE). В параметре lParam или Param1 передается указатель заголовка обработанного буфера данных. В параметре wParam сообщения дополнительно передается ключ объекта, к которому относится событие.
- Достижение заданной позиции в потоке (POSITIONCB). Возникает, когда интерпретатор потока вывода встречает описатель события, имеющий в байте состояния флаг MEVT_F_CALLBACK. При уведомлении передаются те же параметры, что и в событии DONE. Поле dwOffset в заголовке буфера содержит смещение внутри буфера, по которому расположен описатель события с флагом MEVT_F_CALLBACK.
Первые два события являются синхронными и возникают точно в момент выполнения функций Open/Close, остальные - асинхронными, возникающими в произвольный момент времени.
Имена констант для кодов событий, передаваемых при уведомлении, имеют различные префиксы в зависимости от вида уведомления: MIM_xxx / MOM_xxx - при использовании функции, MM_MIM_xxx / MM_MOM_xxx - при использовании задачи или окна. Аббревиатура MIM используется в константах сообщений от устройств ввода, MOM - от устройств или потоков вывода. Например: MM_MIM_LONGDATA - код сообщения окну или задаче от устройства ввода, MOM_CLOSE - сообщение функции от устройства или потока вывода.
Разница между событиями DATA и MOREDATA заключается в ограничениях на время их обработки. Событие DATA возбуждается драйвером, когда его входной буфер пуст или почти пуст, и приложение может потратить на обработку несколько десятков миллисекунд. Событие MOREDATA возбуждается, если буфер практически заполнен, и обработка событий должна выполняться максимально быстро. При получении события MOREDATA лучшим действием приложения будет просто занести MIDI–сообщение в буфер с быстрым доступом и немедленно закончить обработку события; запрещается даже обращаться к функции PostMessage. После освобождения входного буфера драйвер возвращается в обычный режим, посылая приложению события DATA.
События MOREDATA возникают только в том случае, если при открывании устройства функцией Open был указан флаг MIDI_IO_STATUS. В этом случае желательно использовать для уведомления не окно, а отдельную задачу с повышенным приоритетом или программную функцию, обращения к которым обрабатываются быстрее и эффективнее.
При использовании в потоке вывода уведомляющих событий с флагом MEVT_F_CALLBACK необходимо иметь в виду, что поле dwOffset в заголовке буфера является общим для всего буфера, и его значение может быть перезаписано, если очередное такое событие встретится интерпретатору до того, как приложение успеет обработать предыдущее.
Набор интерфейсных функций подсистемы MIDI
Как и в статье, описывающей базовую звуковую подсистему, я буду придерживаться универсальной системы именования функций, указывая лишь смысловую часть имени, и опуская префикс, содержащий тип и "ориентацию" устройства. Например, говоря о функции GetDevCaps, я буду подразумевать две функции - midiInGetDevCaps и midiOutGetDevCaps, а в случае функции Open - сразу три: midiInOpen, midiOutOpen, midiStreamOpen. При этом будут оговариваться лишь их различия для объектов разного типа. Это потребует от читателя "конструирования" полного имени функции в каждом конкретном случае, однако позволит подойти к описанию более широко и систематически. В прототипе функции префикс будет обозначаться последовательностью "xxx".
Первым параметром большинства функций указывается ключ (handle) открытого устройства или потока, имеющий тип HMIDIIN, HMIDIOUT или HMIDISTRM; в прототипе его тип обозначается HMIDIx. Для экономии места этот параметр не упоминается при описании каждой из функций.
Как уже говорилось, ключи всех объектов можно хранить в переменных совместимого типа HMIDI, если не определена макропеременная STRICT, включающая строгую проверку типов Windows.
Перечень интерфейсных функций
GetNumDevs | Запрос количества устройств |
---|---|
GetDevCaps | Запрос параметров и возможностей устройства |
Open | Открывание устройства/потока |
Close | Закрывание устройства/потока |
ShortMsg | Вывод короткого сообщения |
PrepareHeader | Подготовка (фиксация в памяти) буфера данных |
UnprepareHeader | Освобождение (снятие фиксации) буфера данных |
AddBuffer / LongMsg / Out | Передача очередного буфера |
Stop / Pause | Остановка передачи данных |
Start / Restart | Запуск передачи данных |
Reset / Stop | Сброс режима передачи данных или потока |
Position | Запрос позиции потока вывода |
Property | Установка/запрос параметров потока вывода |
SetVolume / GetVolume | Установка/запрос громкости звука на выходе синтезатора |
CachePatches / CacheDrumPatches | Управление набором тембров в памяти синтезатора |
Connect / Disconnect | Установка/разрыв виртуального соединения портов |
GetID | Запрос номера устройства по ключу |
GetErrorText | Запрос текста сообщения об ошибке по коду |
Message | Передача драйверу нестандартного сообщения |
CallbackProc | Шаблон функции уведомления |
Значения, возвращаемые интерфейсными функциями
За редким исключением, все функции интерфейса возвращают результат типа MMRESULT, эквивалентный типу UINT. Значение MMSYSERR_NOERROR, равное нулю, означает успешное выполнение функции, любое другое значение указывает на ошибку. Константы для кодов ошибок имеют префиксы MMSYSERR_ (общая ошибка мультимедийной подсистемы) и MIDIRR_ (ошибка драйвера MIDI–устройства):
MMSYSERR_BADDEVICEID | Недопустимый номер устройства |
---|---|
MMSYSERR_NOTENABLED | Драйвер не активизирован |
MMSYSERR_ALLOCATED | Устройство занято другим приложением |
MMSYSERR_INVALHANDLE | Недопустимый ключ открытого устройства |
MMSYSERR_NODRIVER | Драйвер отсутствует |
MMSYSERR_NOMEM | Недостаточно памяти |
MMSYSERR_NOTSUPPORTED | Запрошенная функция не поддерживается |
MMSYSERR_BADERRNUM | Код ошибки вне допустимого диапазона |
MMSYSERR_INVALFLAG | Недопустимый флаг |
MMSYSERR_INVALPARAM | Недопустимый параметр |
MMSYSERR_HANDLEBUSY | Над ключом выполняется операция от другой задачи (thread) |
MMSYSERR_ERROR | Неопределенная ошибка |
MMSYSERR_NODRIVERCB | Драйвер не выполнил уведомления (callback) |
MIDIERR_UNPREPARED | Заголовок буфера не подготовлен функцией PrepareHeader |
MIDIERR_STILLPLAYING | В потоке/устройстве идет воспроизведение |
MIDIERR_NOMAP | В MIDI Mapper нет карты инструментов |
MIDIERR_NOTREADY | Устройство не готово |
MIDIERR_NODEVICE | Устройства нет в системе |
MIDIERR_INVALIDSETUP | Недопустимый файл параметров MIDI Mapper |
MIDIERR_BADOPENMODE | Недопустимый режим открывания (попытка послать неполное сообщение параллельно потоку вывода) |
Описание интерфейсных функций
Макрос MEVT_EVENTTYPE - выделение кода MIDI–события
Служит для выделения кода события из комплексного поля dwEvent описателя события в потоке вывода.
BYTE MEVT_EVENTTYPE (DWORD Event);
- Event - упакованное представление события в структуре MIDIEVENT.
Возвращает байт кода события и дополнительных флагов.
Фактически макрос выделяет из двойного слова старший байт и реализован при помощи выражения:
#define MEVT_EVENTTYPE(x) ((BYTE) (((x)>>24)&0xFF))
Макрос MEVT_EVENTPARM - выделение параметров MIDI–события
Служит для выделения параметров события из упакованного поля dwEvent описателя события в потоке вывода.
DWORD MEVT_EVENTPARM (DWORD Event);
- Event - упакованное представление события в структуре MIDIEVENT.
Возвращает 24-разрядное слово параметров события.
Фактически макрос выделяет из двойного слова три младших байта и реализован при помощи выражения:
#define MEVT_EVENTPARM(x) ((DWORD) ((x)&0x00FFFFFFL))
GetNumDevs - запрос количества устройств
UINT xxxGetNumDevs (void);
Возвращает количество установленных в системе устройств ввода или вывода.
GetDevCaps - запрос параметров и возможностей устройств
MMRESULT xxxGetDevCaps ( UINT DevId, MIDIxCAPS *Caps, UINT CapsSize );
Служит для определения параметров и возможностей устройства.
- DevId - номер устройства, начиная с нуля, либо ключ ранее открытого устройства, либо константа MIDI_MAPPER. В последнем случае возвращаются параметры стандартного системного устройства.
- Caps - указатель структуры типа MIDIINCAPS или MIDIOUTCAPS.
- CapsSize - размер структуры в байтах.
При успешном завершении функция заполняет поля переданной указателем структуры параметрами устройства. Если были запрошены параметры MIDI Mapper - в качестве имени устройства возвращается название службы переназначения.
Open - открывание устройства или потока вывода
MMRESULT xxxOpen ( HMIDIx *ForHandle, UINT DevId, DWORD Callback, DWORD Instance, DWORD OpenFlags ); MMRESULT midiStreamOpen ( HMIDISTRM *ForHandle, UINT *DevId, DWORD Midi, DWORD Callback, DWORD Instance, DWORD OpenFlags );
- ForHandle - указатель переменной типа HMIDIIN, HMIDIOUT или HMIDISTRM, в которую при успешном завершении операции записывается ключ открытого объекта.
- DevId - номер устройства, начиная с нуля, либо ключ ранее открытого устройства, либо значение MIDI_MAPPER (-1). В последнем случае все операции переадресуются службой переназначения на устройство, заданное в ее настройках.
- В функции открывания потока вывода параметр DevId является указателем переменной, содержащей одно из перечисленных значений.
- Midi - зарезервированный параметр в функции открывания потока вывода, должен иметь значение 1.
- Callback - объект, которому будут передаваться уведомления подсистемы. Задается ключом (handle) окна или события, указателем функции, либо идентификатором задачи (thread id).
- Instance - 32-разрядное информационное слово, которое будет передаваться в параметрах вызова функции уведомления. Например, при разработке универсального интерфейса с разными устройствами это может быть указатель описателя устройства (структуры или объекта класса).
- OpenFlags - вид уведомления дополнительные флаги. Константы видов уведомления имеют префикс CALLBACK_:
NULL | Уведомления не используются. Этот режим берется по умолчанию. |
---|---|
EVENT | Параметр Callback является ключом объекта события (event handle). Может использоваться только с выводным устройствами или потоком. |
THREAD | Параметр Callback является идентификатором задачи (thread id). |
WINDOW | Параметр Callback является ключом окна (window handle). |
FUNCTION | Параметр Callback является указателем функции. |
При открывании устройств ввода в параметре OpenFlags дополнительно может быть задан флаг MIDI_IO_STATUS, требующий от драйвера слежения за состоянием входного буфера и уведомления о его близком переполнении событиями типа MOREDATA.
В случае успешного открывания объекта подсистема возвращает в переменную, на которую ссылается указатель ForHandle, ключ (handle) открытого объекта.
Устройства ввода открываются в режиме "стоп", поступающие на вход интерфейса сообщения не будут получены приложением, если не вызвана функция Start. Потоки вывода также открываются в остановленном режиме, и для их запуска требуется функция Restart.
Устройства вывода сразу после открывания готовы к передачи, и вывода сообщений достаточно выполнить нужную функцию (ShortMsg/LongMsg).
При открывании потока вывода указанное устройство вывода открывается автоматически, и автоматически же закрывается при закрывании потока.
При завершении работы с устройством его необходимо закрыть функцией Close, в противном случае открытое устройство может "зависнуть". В отличие от файловой, подсистема MIDI в Windows гораздо более чувствительна к ошибкам, и не всегда в состоянии отследить завершение программы, чтобы аварийно закрыть все устройства.
Close - закрывание устройства или потока
MMRESULT xxxClose (HMIDIx Handle);
Закрывает устройство или поток вывода. Закрытие допустимо только после завершения обмена данными и возврата приложению всех переданных ранее буферов, иначе подсистема возвращает код ошибки MIDIRR_STILLPLAYING. Поэтому перед вызовом функции необходимо либо дождаться возврата всех буферов в обычном порядке, либо вызвать функцию Reset (для устройства) или Stop (для потока), в результате чего буферы будут возвращены немедленно.
PrepareHeader - подготовка буфера данных и его заголовка
MMRESULT xxxPrepareHeader (HMIDIx Handle, MIDIHDR *Hdr, UINT HSize);
- Hdr - указатель заголовка буфера данных устройства ввода или вывода.
- HSize - размер структуры заголовка.
Подготавливает буфер данных к передаче драйверу. Обычно подготовка заключается в фиксации буфера в памяти, чтобы во время внепроцессорной передачи (DMA) он не оказался вытесненным (откачанным) на диск. В слове состояния заголовка подготовленного буфера подсистемой устанавливается флаг MHDR_PREPARED.
Перед вызовом функции в заголовке буфера должны быть заполнены поля lpData, dwBufferLength, dwFlags.
Для буферов данных потоков вывода предварительной подготовки не требуется.
После завершения обработки буфера данных и до его освобождения функциями управления памятью фиксация должна быть снята функцией UnprepareHeader.
Для уже подготовленного буфера функция не выполняет никаких действий и завершается успешно.
UnprepareHeader - отмена подготовительных действий для буфера
MMRESULT xxxUnprepareHeader (HMIDIx Handle, MIDIHDR *Hdr, UINT HSize);
- Hdr - указатель заголовка буфера данных.
- HSize - размер структуры заголовка.
Отменяет подготовительные действия, выполненные ранее функцией PrepareHeader. Обычно это состоит в снятии режима фиксации буфера в памяти.
При успешном выполнении функции в слове состояния заголовка буфера сбрасывается флаг MHDR_PREPARED.
Для неподготовленного буфера функция не выполняет никаких действий и завершается успешно.
ShortMsg - вывод короткого сообщения
MMRESULT midiOutShortMsg (HMIDIOUT Handle, DWORD Msg);
- Msg - короткое MIDI–сообщение в упакованном виде.
Выводит заданное сообщение на устройство либо в поток вывода. В последнем случае параметр Handle задает ключ потока.
Сообщение может быть оформлено либо полностью (с байтом состояния), либо в соответствии с правилами режима Running Status. Если сообщение выводится в поток вывода - оно обязательно должно быть полным, иначе возвращается код ошибки MIDIERR_BADOPENMODE.
Синхронный MIDI–драйвер не возвращает управления до тех пор, пока все сообщение не будет полностью обработано.
AddBuffer/LongMsg/Out - передача буфера данных устройству или потоку
MMRESULT midiInAddBuffer (HMIDIIN Handle, MIDIHDR *Hdr, UINT HSize); MMRESULT midiOutLongMsg (HMIDIOUT Handle, MIDIHDR *Hdr, UINT HSize); MMRESULT midiStreamOut (HMIDISTRM Handle, MIDIHDR *Hdr, UINT HSize);
- Hdr - указатель заголовка буфера данных.
- HSize - размер структуры заголовка.
Передает буфер данных для ввода длинного сообщения (AddBuffer), вывода длинного сообщения (LongMsg), либо для вывода серии сообщений потока (Out). В первых двух случаях буфер должен быть подготовлен функцией PrepareHeader, иначе драйвер откажется его принять.
Для потоков вывода размер буфера не должен превышать 64 кб.
Получив буфер, подсистема сбрасывает в его заголовке флаг WHDR_DONE, включает заголовок во внутреннюю очередь и устанавливает флаг WHDR_INQUEUE. После этого асинхронный драйвер возвращает управление приложению, продолжая параллельную обработку очереди буферов по прерываниям от устройства; синхронный драйвер возвращает управление лишь после завершения обработки буфера.
Завершив обработку очередного буфера, драйвер изымает его из очереди, сбрасывает флаг WHDR_INQUEUE, затем устанавливает флаг WHDR_DONE, после чего выполняет уведомление приложения, если это было запрошено при открывании устройства. Затем драйвер продолжает обработку следующего буфера из очереди.
Приложение не имеет права изменять какие-либо поля заголовка до тех пор, пока обработка буфера драйвером не будет завершена.
Поскольку заголовок буфера имеет только одно поле для связывания в список, повторная передача драйверу буфера, уже помещенного в очередь, приводит к ошибке.
Stop/Pause - остановка ввода сообщений/воспроизведения потока
MMRESULT midiInStop (HMIDIIN Handle); MMRESULT midiStreamPause (HMIDISTRM Handle);
Для устройства ввода запрещается прием входящих сообщений. Не до конца заполненный текущий буфер данных немедленно возвращается программе; остальные буферы, если они есть, остаются в очереди.
Для потока вывода временно прекращается обработка последовательности событий на текущей позиции потока. Все буферы данных остаются в очереди.
При остановленном потоке функция не выполняет никаких действий и завершается успешно.
Start/Restart - запуск ввода/воспроизведения потока
MMRESULT midiInStart (HMIDIIN Handle); MMRESULT midiStreamRestart (HMIDISTRM Handle);
Для устройства ввода разрешает прием входящих сообщений. Счетчик времени, относительно которого помечаются входящие сообщения (timestamp), устанавливается в нуль.
Для потока вывода запускает обработку последовательности событий с текущей позиции потока.
При активном потоке функция не выполняет никаких действий и завершается успешно.
Reset/Stop - сброс устройства или остановка потока
MMRESULT xxxReset (HMIDIx Handle); MMRESULT midiStreamStop (HMIDISTRM Handle);
Сбрасывает всю текущую активность устройства (Reset) или потока вывода (Stop). Все буферы данных, находящиеся в очереди, немедленно возвращаются приложению. Для устройства ввода запрещается дальнейший прием входящих сообщений. Для внутреннего синтезатора выполняется сброс всех потенциально зависших нот, для потока вывода отключаются только те ноты, которые были активизированы событиями потока. Позиция потока обнуляется.
Вызов функции midiOutReset может прервать вывод сообщения SysEx - в этом случае устройство, которому предназначено сообщение, не получит завершающего сообщения EOX.
Position - запрос текущей позиции потока
MMRESULT midiStreamPosition (HMIDISTRM Handle, MMTIME *Time, UINT TSize);
- Time - указатель структуры типа MMTIME. В поле wType должен быть установлен код единиц, в которых запрашивается позиция.
- TSize - размер структуры в байтах.
Возвращает текущую позицию потока, заполняя поля переданной структуры в соответствии со значением поля wType. Если драйвер не в состоянии вернуть позицию в требуемых единицах, он по своему усмотрению устанавливает значение поля wType и заполняет структуру в выбранных им единицах.
SetVolume - установка громкости звука на выходе синтезатора
MMRESULT midiOutSetVolume (HMIDIOUT Handle, DWORD Volume);
- Volume - громкость по левому и правому каналу. Младшее слово задает громкость левого канала, старшее - правого. Значение 0xFFFF задает максимальную громкость, 0 - минимальную. Для драйверов, не поддерживающих независимую регулировку громкости по каналам, младшее слово задает громкость в обоих каналах синтезатора.
Функция устанавливает уровень выходного сигнала синтезатора. Несмотря на то, что функцией допускается 65536 уровней громкости, синтезатор может поддерживать гораздо меньше - 8, 32 или 128 уровней. В таких случаях значащими является только от трех до семи старших разрядов значения громкости, младшие разряды игнорируются. Такая трактовка позволяет использовать одну и ту же шкалу громкости, изменяя лишь степень ступенчатости регулировки.
Функция поддерживается только драйверами, в свойствах которых установлен флаг MIDICAPS_VOLUME. Раздельная регулировка по каналам поддерживается только при наличии флага MIDICAPC_LRVOLUME.
GetVolume - запрос текущей громкости воспроизведения
MMRESULT midiOutGetVolume (HMIDIx Handle, LPDWORD ForVolume);
- ForVolume - указатель переменной типа DWORD, в которую заносятся текущие уровни громкости.
Функция опрашивает текущий установленный уровень выходного сигнала. Трактовка переменной, на которую ссылается указатель ForVolume, аналогична используемому в функции SetVolume.
Property - запрос/установка параметров потока
MMRESULT midiStreamProperty ( HMIDISTRM Handle, BYTE *Data, DWORD Op );
- Data - указатель области данных, в которой находится устанавливаемый параметр потока вывода, либо в которую записывается запрашиваемый параметр потока. Поскольку описатели параметров имеют разные типы, требуется явное приведение к типу (BYTE *).
- Op - вид операции и тип параметра. Константы для этих кодов имеют префикс MIDIPROP_ и объединяются операцией "логическое Или".
Виды операций с параметрами:
GET | Запрос значения параметра |
---|---|
SET | Установка значения параметра |
Типы параметров:
TEMPO | Темп воспроизведения потока (описатель MIDIPROPTEMPO). Может быть установлен в любой момент. События MEVT_TEMPO в потоке приводят к изменению текущего темпа. |
---|---|
TIMEDIV | Временн_а_я шкала потока (описатель MIDIPROPTIMEDIV). Может быть установлен только при остановленном потоке. |
Запрос параметров потока может выполняться в любой момент времени.
Connect/Disconnect - установка/разрыв виртуального соединения
MMRESULT midiConnect (HMIDI In, HMIDIOUT Out, VOID *Reserved); MMRESULT midiDisconnect (HMIDI In, HMIDIOUT Out, VOID *Reserved);
- In - ключ устройства ввода.
- Out - ключ устройства вывода.
- Reserved - резервный параметр, должен иметь нулевое значение.
Функция Connect выполняет виртуальное соединение двух устройств - вводного и выводного, имитируя аппаратную реализацию входного ретранслятора MIDI–интерфейса (Thru). При появлении сообщений в устройстве ввода они автоматически будут дублироваться указанному устройству вывода.
В интерфейсе подсистемы с MIDI–драйвером поддержка этой функции предполагает возможность подключения к одному устройству ввода нескольких устройств вывода. Если драйвер не поддерживает этой функции, подсистема MIDI сама выполняет пересылку сообщений, но позволяет соединить только два устройства.
Функция Disconnect разрывает ранее установленное соединение устройств.
Надо сказать, что мне не удалось добиться описанной в документации работы этого механизма на практике.
CachePatches/CacheDrumPatches - управление набором тембров (патчей)
MMRESULT midiOutCachePatches ( HMIDIOUT Handle, UINT Bank, WORD *PatchArray, UINT Op ); MMRESULT midiOutCacheDrumPatches ( HMIDIOUT Handle, UINT Patch, WORD *KeyArray, UINT Op );
- Bank / Patch - номер банка мелодических инструментов (для CachePatches) или набора ударных инструментов (для CacheDrumPatches). В случае обращения к банку или набору по умолчанию параметр должен быть нулевым.
- PatchArray / KeyArray - массив данных мелодических тембров (для CachePatches) или нот ударных звуков (для CacheDrumPatches). Массив имеет тип WORD, его длина определяется константой MIDIPATCHSIZE (128). Для удобства определены типы PATCHARRAY и KEYARRAY.
- Каждый элемент массива определяет набор MIDI–каналов, в которых используется соответствующий номеру элемента тембр (звук). Отсчет номеров тембров, как и элементов массива, ведется с нуля. Использующие тембр каналы отмечаются установленными разрядами в элементе массива, каналу 1 соответствует бит 0. Например, если тембр с номером 17 используется каналами 1 и 4, 17-й элемент массива должен быть равен 9 (двоичное 1001).
- Op - код выполняемой операции. Константы для кодов операций имеют префикс MIDI_:
CACHE_ALL | Загрузка всех указанных тембров. Если памяти синтезатора не хватает для загрузки всего приведенного списка - функция не загружает ничего, обнуляет массив тембров и возвращает код MMSYSERR_NOMEM. |
---|---|
CACHE_BESTFIT | Загрузка максимально возможного подмножества тембров. Функция пытается загрузить наибольшее возможное количество тембров; если памяти синтезатора недостаточно - в массиве обнуляются элементы незагруженных тембров и возвращается код MMSYSERR_NOMEM. |
CACHE_QUERY | Запрос множества тембров, загруженных в настоящее время. Массив заполняется данными о загруженных тембрах. |
UNCACHE | Удаление из памяти синтезатора тембров, отмеченных в массиве. После выполнения операции массив обнуляется. |
Таким образом, каждая операция поддерживает массив тембров в виде, отражающем текущее состояние памяти синтезатора.
GetID - запрос номера устройства по ключу
MMRESULT xxxGetID (HMIDIx Handle, UINT *ForID);
- ForID - указатель переменной типа UINT, в которую заносится номер устройства.
Функция определяет номер устройства, при открывании которого системой был возвращен заданный ключ. В том случае, если при открывании была использована служба переназначения (значение MIDI_MAPPER вместо номера) функция возвращает значение MIDI_MAPPER.
Документация Microsoft утверждает, будто эта функция поддерживается только для совместимости, и для получения номера достаточно привести ключ к нужному типу, однако это совсем не так. Ключ открытого устройства является адресом описателя, принадлежащего подсистеме, и в Win32 размещается в общей области памяти. Единственный способ получить номер устройства по ключу - использование функции GetID.
GetErrorText - запрос текстового сообщения об ошибке по коду
MMRESULT xxxGetErrorText (MMRESULT Error, char *Buf, UINT Size);
- Error - код ошибки, возвращенный одной из интерфейсных функций.
- Buf - указатель текстового буфера (массива типа char).
- Size - размер текстового буфера в байтах.
Функция заносит в заданный буфер текстовое описание ошибки с заданным кодом. Записанный текст завершается нулевым байтом. Если буфер недостаточно велик - конец текста обрезается; нулевой байт записывается в буфер в любом случае. Размер буфера, способного вместить любое сообщение об ошибке, определяется константой MAXERRORLENGTH.
Сообщения об ошибках не имеют разделения по типам устройств, поэтому для запроса текста любой ошибки достаточно любой из возможных функций - например, midiOutGetErrorText.
Message - передача сообщения драйверу устройства
MMRESULT xxxMessage (HMIDIx Handle, UINT Msg, DWORD P1, DWORD P2);
- Msg - код передаваемого сообщения.
- P1, P2 - параметры сообщения.
Функция используется для прямой передачи сообщения драйверу. Все интерфейсные функции, кроме GetID и GetErrorText и функций работы с потоками вывода, транслируются подсистемой в сообщения, передаваемые драйверу; при этом каждое сообщение имеет два параметра типа DWORD, в которые преобразуются параметры интерфейсных функций. Если драйвер устройства поддерживает нестандартные сообщения - они могут быть переданы ему при помощи функций Message. Возвращаемое значение при этом определяется самим драйвером.
CallbackProc - функция приложения, вызываемая при уведомлении
Эта функция определяется приложением, подсистема MIDI вызывает ее при выполнении уведомлений, передавая в ее аргументах код и параметры события. Прототип функции имеет следующий вид (имя CallbackProc приведено для примера, реальное имя выбирается приложением):
void CALLBACK CallbackProc (
HMIDIx Handle, UINT Msg, DWORD Instance, DWORD Param1, DWORD Param2 );
- Handle - ключ устройства. Имеет тип HMIDIIN или HMIDIOUT; допустимо использование универсального типа HMIDI, если не определена макропеременная STRICT.
- Msg - код типа события.
- Instance - 32-разрядное информационное слово, указанное программой при открывании устройства. Подсистема и драйвер никак не используют это значение, а лишь передают его при каждом вызове функции.
- Param1, Param2 - параметры события.
В Win16 функция может вызываться в контексте обработчика прерывания, поэтому безопасно может использовать лишь ограниченный набор функций Windows: EnterCriticalSection, LeaveCriticalSection, midiOutLongMsg, midiOutShortMsg, OutputDebugString, PostMessage, PostThreadMessage, SetEvent, timeGetSystemTime, timeGetTime, timeKillEvent, timeSetEvent. Обращение к другим системным функциям, как и к функциям подсистемы, может вызвать непредсказуемые последствия.
Обратите внимание, что функция-обработчик может обращаться к функциям midiOutShortMsg, midiOutLongMsg. Это означает, что логика переназначения/отражения MIDI–сообщений может быть реализована непосредственно в этой функции.
В Win32 для вызова функции подсистема создает отдельную задачу (thread) с повышенным (ABOVE_NORMAL) приоритетом. В отличие от передачи сообщений окну/задаче, которые обрабатываются в порядке очереди, вызов функции происходит параллельно с работой остальных задач процесса, поэтому необходимо заботиться о синхронизации доступа функции и других задач к общим переменным и структурам данных.
Вспомогательная задача создается один раз и существует до полного завершения процесса. Подсистема MIDI вызывает из этой задачи функции уведомления для всех устройств, которые будут открыты за время жизни процесса.
Недостатки подсистемы MME/MIDI
В Windows 95/98 подсистема MME и ее драйверы так и остались 16-разрядными, как и в Windows 3.x. Из-за этого каждое обращение к драйверу из Win32–приложения сопровождается двойной сменой режима исполнения (thunking), приводящее, увы, к дополнительным накладным расходам, доходящим до единиц миллисекунд.
Кроме этого, вся мультимедийная подсистема Windows 3.x и 95/98 отличается низкой устойчивостью к ошибкам. Это чаще всего проявляется в том, что при аварийном завершении программы, открывшей устройства и работающей с ними, система не выполняет корректного закрытия (cleanup) используемых устройств. В результате этого в ряде случаев после такого аварийного завершения может потребоваться перезагрузка, а до тех пор незакрытые устройства будут недоступны другим приложениям. Кроме этого, 16-разрядные подсистемы защищены от ошибок гораздо меньше 32-разрядных, так что серьезные ошибки в звуковых программах могут приводить к сбоям и зависаниям всей системы Windows.
В Windows NT/2000 все подсистемы сделаны изначально 32-разрядными, так что описанных проблем там не возникает.
Пример программы, использующий интерфейс MIDI
Для иллюстрации приводится программа MIDIKEYB, имитирующая работу MIDI-клавиатуры. В диалоговом окне требуется выбрать устройство MIDI–вывода, номер канала и название тембра в стандарте General MIDI.
Алфавитно-цифровые клавиши ZXCVBNM соответствуют белым клавишам первой фортепианной октавы, а клавиши QWERTYU - белым клавишам второй октавы. Расположенные выше них клавиши SD GHJ и 23 567 соответствуют черным клавишам.
Как и на MIDI–клавиатуре, нажатие клавиши запускает звучание ноты, а отпускание - прекращает его. Возможна игра аккордами, однако на различных типах клавиатур есть собственные ограничения на количество и расположение клавиш, которые могут быть нажаты одновременно.
Список тембров можно не закрывать, перемещаясь по нему клавишами "Вверх" и "Вниз" - тембры при этом будут переключаться автоматически.
При нажатии недопустимой для клавиатуры комбинации возможно пропадание отдельных клавиатурных кодов, и как следствие - зависание звучащих нот. Для их сброса служит кнопка "Сброс".
Канал 10 в большинстве синтезаторов работает в режиме ударных звуков, поэтому нажатия клавиш будут давать соответствующие им ударные звуки из стандартного набора. Смена тембра в этом канале не приводит к изменению звучания, если синтезатор не использует эту команду для переключения банков.
Программа реализована на языке C++ без использования классов, MFC и RTTI. Прилагаются исходные тексты и исполняемый файл.
Разработка программы выполнялась в среде MS VC++ 4.2. Использован только стандартный интерфейс (API) Windows, без каких-либо расширений из среды разработки.
Для получения кодов клавиш ранее, чем они попадут стандартному обработчику диалогов, используется немодальный (modeless) диалог в сочетании со стандартным циклом обработки оконных сообщений. Сообщения о нажатии/отпускании клавиш "вылавливаются" в этом цикле, остальные сообщения направляются стандартному обработчику.
Для совместимости с Gravis Ultrasound поддерживается механизм загрузки тембров.
Для работы программы необходим любой MIDI–синтезатор - внешний, внутренний либо виртуальный. Вся работа с MIDI ведется в соответствии со спецификацией MIDI 1.0 и General MIDI 1.0.
---
Любой из материалов, опубликованных на этом сервере, не может быть воспроизведен в какой бы то ни было форме и какими бы то ни было средствами без письменного разрешения владельцев авторских прав.