Программирование подсистемы MIDI

Автор: Евгений Музыченко
Источник: Компьютер Пресс
Опубликовано: 19.07.2003
Версия текста: 1.0

Краткие сведения об аппаратном устройстве MIDI–интерфейса
Основные черты и понятия интерфейса и подсистемы MIDI
Партитура и звук
Каналы и тембры
Номера и звучание тембров
Кэширование загружаемых тембров
Реальные и виртуальные синтезаторы
Внешние и внутренние синтезаторы
Синхронные и асинхронные драйверы
Управление громкостью
Сходство подсистем MIDI и Audio/Wave
Структура MIDI–сообщений
Режим Running Status
Отличие потока MIDI–данных от потока цифрового звука
Временная шкала и темп потока вывода
Непосредственный и буферизованный ввод/вывод сообщений
Автоматизированный потоковый вывод в подсистеме MIDI
Подготовка буферов данных
Зависание нот
Программные события и уведомление о них
Дуплекс
Поддержка нескольких клиентов
Служба переназначения устройств, каналов, тембров и клавиш
Номера MIDI–устройств
Идентификаторы (ключи) открытых устройств
Имена интерфейсных функций
Общая схема взаимодействия программы и подсистемы MIDI
Средства разработки, включаемые файлы и библиотеки
Структуры, используемые в интерфейсе
Структуры MIDIINCAPS и MIDIOUTCAPS
Структура MIDIHDR
Структура упакованного короткого сообщения
Структура MIDIEVENT
Структура MMTIME
Структуры MIDIPROPTEMPO и MIDIPROPTIMEDIV
Структура MIDISTRMBUFFVER
Уведомления, передаваемые программе подсистемой MIDI
Типы событий, о которых сообщается приложению
Набор интерфейсных функций подсистемы MIDI
Перечень интерфейсных функций
Значения, возвращаемые интерфейсными функциями
Описание интерфейсных функций
Макрос MEVT_EVENTTYPE - выделение кода MIDI–события
Макрос MEVT_EVENTPARM - выделение параметров MIDI–события
GetNumDevs - запрос количества устройств
GetDevCaps - запрос параметров и возможностей устройств
Open - открывание устройства или потока вывода
Close - закрывание устройства или потока
PrepareHeader - подготовка буфера данных и его заголовка
UnprepareHeader - отмена подготовительных действий для буфера
ShortMsg - вывод короткого сообщения
AddBuffer/LongMsg/Out - передача буфера данных устройству или потоку
Stop/Pause - остановка ввода сообщений/воспроизведения потока
Start/Restart - запуск ввода/воспроизведения потока
Reset/Stop - сброс устройства или остановка потока
Position - запрос текущей позиции потока
SetVolume - установка громкости звука на выходе синтезатора
GetVolume - запрос текущей громкости воспроизведения
Property - запрос/установка параметров потока
Connect/Disconnect - установка/разрыв виртуального соединения
CachePatches/CacheDrumPatches - управление набором тембров (патчей)
GetID - запрос номера устройства по ключу
GetErrorText - запрос текстового сообщения об ошибке по коду
Message - передача сообщения драйверу устройства
CallbackProc - функция приложения, вызываемая при уведомлении
Недостатки подсистемы MME/MIDI
Пример программы, использующий интерфейс MIDI

Демонстрационная программа

Вместе с подсистемой собственно цифрового звука (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 содержит службу переназначения - MIDIMapper. Существует понятие стандартного системного устройства ввода и стандартного системного устройства вывода, которые задаются в закладке 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 имеет одно дополнительное поле:

DWORD  dwSupport;

Индивидуальная часть структуры MIDIOUTCAPS имеет пять дополнительных полей:

WORD  wTechnology;
WORD  wVoices;
WORD  wNotes;
WORD  wChannelMask;
DWORD  dwSupport;
VOLUME Поддержка управления громкостью.
LRVOLUME Поддержка независимого по каналам управления громкостью.
STREAM Прямая поддержка драйвером потоков вывода.
CACHE Поддержка кэширования тембров.
MIDIPORT Аппаратный MIDI–порт.
MAPPER Порт службы переназначения.
SYNTH Внутренний синтезатор с технологией неизвестного типа.
FMSYNTH Внутренний синтезатор с частотно-модуляционной технологией.
SQSYNTH Внутренний синтезатор выходными сигналами в форме меандра (square wave).

Внутренние таблично-волновые синтезаторы имеют тип MOD_SYNTH.

Структура MIDIHDR

Называется заголовком буфера и описывает буфер - как хранилище данных и как элемент очереди драйвера. Изначально заполняется приложением, впоследствии отдельные поля модифицируются драйвером.

LPSTR  lpData;
DWORD  dwBufferLength;
DWORD  dwBytesRecorded;
DWORD  dwUser;
DWORD  dwFlags;
LPMIDIHDR  lpNext;
DWORD  reserved;
DWORD  dwOffset;
DWORD  dwReserved [4];
PREPARED Буфер подготовлен (зафиксирован в памяти)
INQUEUE Буфер находится в очереди драйвера
DONE Обработка буфера драйвером завершена
ISSTRM Буфер принадлежит потоку вывода

Структура упакованного короткого сообщения

Короткие сообщения передаются упакованными в переменные типа DWORD. По сути, сообщение просто записывается в область памяти, отведенную под эту переменную, начиная с первого (младшего) байта. Таким образом, первый байт сообщения (это байт состояния, если не используется Running Status) попадает в младший байт переменной, а следующие байты сообщения - в следующие по старшинству байты переменной. Поскольку длина коротких сообщений не превышает трех байтов, старший байт переменной сообщением никогда не используется.

По отдельности байты сообщения из упакованной переменной можно извлекать, поочередно сдвигая ее значение вправо на 8 разрядов, либо приведя к типу указателя на байт (char * или BYTE *) , и последовательно выбирая байты в порядке возрастания адресов.

Структура MIDIEVENT

Описывает мета-событие в потоке вывода. Заполняется приложением.

DWORD  dwDeltaTime;
DWORD  dwStreamID;
DWORD  dwEvent;
DWORD  dwParms [];

Флаги, указываемые вместе с кодом события:

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 Пустое событие (короткое представление). Поле параметров игнорируется.

Структура 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;
BYTES Количество байтов от начала потока
MIDI Время в стандарте MIDI (Sond Position Pointer)
MS Время в миллисекундах
SAMPLES Количество звуковых блоков с начала звукового (Audio) потока
SMPTE Время в стандарте SMPTE (в кадрах)
TICKS Время в тиках от начала MIDI–потока

Структуры MIDIPROPTEMPO и MIDIPROPTIMEDIV

Описывают свойства потока вывода - темп и временную шкалу. Формат структуры MIDIPROPTEMPO:

DWORD cbStruct;
DWORD dwTempo;

Формат структуры MIDIPROPTIMEDIV:

DWORD cbStruct;
DWORD dwTimeDiv;

Структура MIDISTRMBUFFVER

Описывает версию потока вывода.

DWORD dwVersion;
DWORD dwMid;
DWORD dwOEMVersion;

Уведомления, передаваемые программе подсистемой MIDI

Для сообщения программе о наступлении различных событий в отношении устройств и потоков подсистема MIDI может использовать три вида уведомлений: установку объекта программного события (event), вызов заданной программной функции (callback), либо посылку сообщения заданному окну (window) или задаче (thread). В первом варианте программа получает информацию лишь о самом факте некоторого события, и сама должна выяснять, что именно произошло; во втором и третьем вариантах передается код события и уточняющая информация (параметры события).

Уведомление при помощи программного события возможно только в отношении объектов вывода (выводной порт или поток вывода).

Типы событий, о которых сообщается приложению

В подсистеме MIDI возможно возникновение шести различных событий. В скобках приведены общие части имен констант, кодирующих виды событий. Имена параметров указаны традиционные - параметры wParam, lParam относятся к сообщеням Windows, а Param1, Param2 - к вызовам 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);

Возвращает байт кода события и дополнительных флагов.

Фактически макрос выделяет из двойного слова старший байт и реализован при помощи выражения:

        #define MEVT_EVENTTYPE(x) ((BYTE) (((x)>>24)&0xFF))

Макрос MEVT_EVENTPARM - выделение параметров MIDI–события

Служит для выделения параметров события из упакованного поля dwEvent описателя события в потоке вывода.

DWORD MEVT_EVENTPARM (DWORD Event);

Возвращает 24-разрядное слово параметров события.

Фактически макрос выделяет из двойного слова три младших байта и реализован при помощи выражения:

        #define MEVT_EVENTPARM(x) ((DWORD) ((x)&0x00FFFFFFL))

GetNumDevs - запрос количества устройств

UINT xxxGetNumDevs (void);

Возвращает количество установленных в системе устройств ввода или вывода.

GetDevCaps - запрос параметров и возможностей устройств

MMRESULT xxxGetDevCaps (
  UINT DevId,
  MIDIxCAPS *Caps,
  UINT CapsSize
);

Служит для определения параметров и возможностей устройства.

При успешном завершении функция заполняет поля переданной указателем структуры параметрами устройства. Если были запрошены параметры MIDIMapper - в качестве имени устройства возвращается название службы переназначения.

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
);
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);

Подготавливает буфер данных к передаче драйверу. Обычно подготовка заключается в фиксации буфера в памяти, чтобы во время внепроцессорной передачи (DMA) он не оказался вытесненным (откачанным) на диск. В слове состояния заголовка подготовленного буфера подсистемой устанавливается флаг MHDR_PREPARED.

Перед вызовом функции в заголовке буфера должны быть заполнены поля lpData, dwBufferLength, dwFlags.

Для буферов данных потоков вывода предварительной подготовки не требуется.

После завершения обработки буфера данных и до его освобождения функциями управления памятью фиксация должна быть снята функцией UnprepareHeader.

Для уже подготовленного буфера функция не выполняет никаких действий и завершается успешно.

UnprepareHeader - отмена подготовительных действий для буфера

MMRESULT xxxUnprepareHeader (HMIDIx Handle, MIDIHDR *Hdr, UINT HSize);

Отменяет подготовительные действия, выполненные ранее функцией PrepareHeader. Обычно это состоит в снятии режима фиксации буфера в памяти.

При успешном выполнении функции в слове состояния заголовка буфера сбрасывается флаг MHDR_PREPARED.

Для неподготовленного буфера функция не выполняет никаких действий и завершается успешно.

ShortMsg - вывод короткого сообщения

MMRESULT midiOutShortMsg (HMIDIOUT Handle, DWORD Msg);

Выводит заданное сообщение на устройство либо в поток вывода. В последнем случае параметр 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);

Передает буфер данных для ввода длинного сообщения (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);

Возвращает текущую позицию потока, заполняя поля переданной структуры в соответствии со значением поля wType. Если драйвер не в состоянии вернуть позицию в требуемых единицах, он по своему усмотрению устанавливает значение поля wType и заполняет структуру в выбранных им единицах.

SetVolume - установка громкости звука на выходе синтезатора

MMRESULT midiOutSetVolume (HMIDIOUT Handle, DWORD Volume);

Функция устанавливает уровень выходного сигнала синтезатора. Несмотря на то, что функцией допускается 65536 уровней громкости, синтезатор может поддерживать гораздо меньше - 8, 32 или 128 уровней. В таких случаях значащими является только от трех до семи старших разрядов значения громкости, младшие разряды игнорируются. Такая трактовка позволяет использовать одну и ту же шкалу громкости, изменяя лишь степень ступенчатости регулировки.

Функция поддерживается только драйверами, в свойствах которых установлен флаг MIDICAPS_VOLUME. Раздельная регулировка по каналам поддерживается только при наличии флага MIDICAPC_LRVOLUME.

GetVolume - запрос текущей громкости воспроизведения

MMRESULT midiOutGetVolume (HMIDIx Handle, LPDWORD ForVolume);

Функция опрашивает текущий установленный уровень выходного сигнала. Трактовка переменной, на которую ссылается указатель ForVolume, аналогична используемому в функции SetVolume.

Property - запрос/установка параметров потока

MMRESULT midiStreamProperty (
  HMIDISTRM Handle,
  BYTE *Data,
  DWORD Op
);

Виды операций с параметрами:

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);

Функция 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
);
CACHE_ALL Загрузка всех указанных тембров. Если памяти синтезатора не хватает для загрузки всего приведенного списка - функция не загружает ничего, обнуляет массив тембров и возвращает код MMSYSERR_NOMEM.
CACHE_BESTFIT Загрузка максимально возможного подмножества тембров. Функция пытается загрузить наибольшее возможное количество тембров; если памяти синтезатора недостаточно - в массиве обнуляются элементы незагруженных тембров и возвращается код MMSYSERR_NOMEM.
CACHE_QUERY Запрос множества тембров, загруженных в настоящее время. Массив заполняется данными о загруженных тембрах.
UNCACHE Удаление из памяти синтезатора тембров, отмеченных в массиве. После выполнения операции массив обнуляется.

Таким образом, каждая операция поддерживает массив тембров в виде, отражающем текущее состояние памяти синтезатора.

GetID - запрос номера устройства по ключу

MMRESULT xxxGetID (HMIDIx Handle, UINT *ForID);

Функция определяет номер устройства, при открывании которого системой был возвращен заданный ключ. В том случае, если при открывании была использована служба переназначения (значение MIDI_MAPPER вместо номера) функция возвращает значение MIDI_MAPPER.

Документация Microsoft утверждает, будто эта функция поддерживается только для совместимости, и для получения номера достаточно привести ключ к нужному типу, однако это совсем не так. Ключ открытого устройства является адресом описателя, принадлежащего подсистеме, и в Win32 размещается в общей области памяти. Единственный способ получить номер устройства по ключу - использование функции GetID.

GetErrorText - запрос текстового сообщения об ошибке по коду

MMRESULT xxxGetErrorText (MMRESULT Error, char *Buf, UINT Size);

Функция заносит в заданный буфер текстовое описание ошибки с заданным кодом. Записанный текст завершается нулевым байтом. Если буфер недостаточно велик - конец текста обрезается; нулевой байт записывается в буфер в любом случае. Размер буфера, способного вместить любое сообщение об ошибке, определяется константой MAXERRORLENGTH.

Сообщения об ошибках не имеют разделения по типам устройств, поэтому для запроса текста любой ошибки достаточно любой из возможных функций - например, midiOutGetErrorText.

Message - передача сообщения драйверу устройства

MMRESULT xxxMessage (HMIDIx Handle, UINT Msg, DWORD P1, DWORD P2);

Функция используется для прямой передачи сообщения драйверу. Все интерфейсные функции, кроме GetID и GetErrorText и функций работы с потоками вывода, транслируются подсистемой в сообщения, передаваемые драйверу; при этом каждое сообщение имеет два параметра типа DWORD, в которые преобразуются параметры интерфейсных функций. Если драйвер устройства поддерживает нестандартные сообщения - они могут быть переданы ему при помощи функций Message. Возвращаемое значение при этом определяется самим драйвером.

CallbackProc - функция приложения, вызываемая при уведомлении

Эта функция определяется приложением, подсистема MIDI вызывает ее при выполнении уведомлений, передавая в ее аргументах код и параметры события. Прототип функции имеет следующий вид (имя CallbackProc приведено для примера, реальное имя выбирается приложением):

        void CALLBACK CallbackProc (
  HMIDIx Handle,
  UINT Msg,	
  DWORD Instance,
  DWORD Param1,
  DWORD 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.


Любой из материалов, опубликованных на этом сервере, не может быть воспроизведен в какой бы то ни было форме и какими бы то ни было средствами без письменного разрешения владельцев авторских прав.