Опубликован: 13.07.2012 | Доступ: свободный | Студентов: 460 / 9 | Оценка: 5.00 / 5.00 | Длительность: 18:06:00
Специальности: Программист
Лекция 19:

Мультимедиа. Работа со звуком

< Лекция 18 || Лекция 19: 12345 || Лекция 20 >

Отправка и обработка сообщения MIDI

MIDI (Musical Instrument Digital Interface) — цифровой интерфейс музыкальных инструментов, созданный в 1982 году и включающий аппаратные и программные средства, предназначенные для управления звуковой платой или звуковым синтезатором. Проще говоря, это способ обмена данными между устройствами либо программами, поддерживающими MIDI. Данными являются ноты, а также дополнительные сведения о них: громкость, длительность и тембр звука, положение в пространстве стереофонического звучания (величина смещения в сторону левой или правой колонки) и другие. Такие данные называются MIDI-сообщениями или MIDI-командами.

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

К программно-аппаратным средствам, или проще, устройствам MIDI относятся:

MIDI-клавиатура (MIDI-контроллер) — MIDI-устройство, предназначенное для выдачи MIDI-данных на другие MIDI-устройства. Как правило, представляет собой инструмент с клавиатурой, похожей на фортепьянную; существуют также "ударные", "щипковые" и др. MIDI-контроллеры.

MIDI-секвенсер (sequencer) — программа для персонального компьютера, обеспечивающая запись, редактирование и размещение нескольких дорожек с MIDI-данными; используется для записи партитуры и воспроизведения музыкальных произведений электронными музыкальными инструментами.

MIDI-синтезатор (tone module) — автономное внешнее (периферийное) устройство, подсоединяемое к цифровому музыкальному инструменту или компьютеру, обеспечивающее музыкальный синтез.

Устройства MIDI связываются между собой посредством кабелей, имеющих 5-штырьковые разъёмы стандарта DIN, в которых используются только 3 контакта из пяти (земля, токовая петля и линия данных). Протокол MIDI определяет пересылку цифровых команд по кабелю. В случае работы с MIDI на персональном компьютере устройства (клавиатура, секвенсер, синтезатор) могут быть реализованными программно (виртуальными).

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

В настоящее время существует несколько стандартов MIDI, и самый распространённый — это General MIDI (GM). Стандарт предусматривает 16 независимых и равноправных логических каналов, имеющих собственные режимы работы. Например, канал номер 10 канал отведён для ударных. Все ноты, посылаемые на этот канал, будут воспроизведены виртуальной ударной установкой MIDI-синтезатора. Каждому каналу обычно назначается свой тембр, называемый по традиции "инструментом". Стандарт определяет так называемую таблицу инструментов, которая не что иное, как пронумерованный список из 128 названий инструментов. Например, под номером 1 идёт акустическое пианино.

Напишем демонстрационную программу, использующую классы Juce для генерации сообщения MIDI и его обработки (воспроизведение ноты). Предусмотрим возможность выбора пользователем устройства MIDI, установленного на его компьютере (синтезатора), а также инструмента (тембра) воспроизведения ноты ( рис. 19.2).

Работа программы, демонстрирующей генерацию и обработку сообщения MIDI

Рис. 19.2. Работа программы, демонстрирующей генерацию и обработку сообщения MIDI

В классе компонента содержимого объявим два выпадающих списка: для выбора пользователем устройств MIDI и инструментов ( пример 19.7). Во второй список мы добавим некоторые из 128 стандартных инструментов; в первый же будут добавляться при запуске программы устройства MIDI, имеющиеся на компьютере пользователя. Для удобства включения их в список предусмотрим в нашем классе метод inline void TCentralComponent::AddInstruments().

#ifndef _TCentralComponent_h_
#define _TCentralComponent_h_
//--------------------------------------------------
#include "../JuceLibraryCode/JuceHeader.h"
//-------------------------------------------------
class TCentralComponent : public Component,
            public ComboBoxListener,
            public ButtonListener
{
public:
  TCentralComponent();
  ~TCentralComponent();

  void paint(Graphics&);
  void resized();
  void comboBoxChanged(ComboBox*);
  void buttonClicked(Button*);

private:
  // Флаг включения / выключения ноты
  bool bIsPlaying;
  // Номер текущего инструмента
  int iInstrument;
  // Выпадающий список устройств MIDI
  ComboBox* pMIDIDevicesBox;
  // Выпадающий список инструментов
  ComboBox* pInstrumentsBox;
  TextButton* pPlayButton;

  // Устройство вывода
  MidiOutput* pMidiOutput;

  // Добавляем новый инструмент в выпадающий список
  inline void AddInstruments();
  // Играем ноту
  void Play();
};
//-------------------------------------------------------
#endif
Листинг 19.7. Объявление класса компонента содержимого TCentralComponent (файл TCentralComponent.h)

Когда на входной разъём синтезатора подаётся сообщение Note On (включить ноту), он реагирует на это воспроизведением соответствующего звука, который будет воспроизводиться до тех пор, пока не получит команду Note Off (выключить ноту).

Команда Note On представляет собой трёхбайтовое сообщение формата 0x90 KK VV, где первый байт (0x90) — это собственно команда включения ноты, второй — номер ноты (точнее, клавиши MIDI-клавиатуры), а третий означает скорость нажатия на клавишу. Номера клавиш варьируют от 0 до 127. В случае, если номер байт KK равен нулю, команда превращается в Note Off.

Например, если синтезатор получает сообщение 0x90 60 100, он воспроизводит ноту до первой октавы (соответствует клавише C 88-клавишной клавиатуры) со скоростью 100.

Для управления командами Note On и Note Off предусмотрим в нашем классе флаг состояния ноты (включена / выключена) - bool bIsPlaying.

Классом-обёрткой для хранения и управления сообщением MIDI в Juce является MidiMessage. В качестве параметров конструктор класса принимает три целых числа — байты сообщения и одну переменную типа double. Последний параметр — время, отводимое сообщению MIDI (по умолчанию принимает значение 0).

Немедленную отправку сообщения (объект класса MidiMessage) MIDI-устройству выполняет метод virtual void MidiOutput::sendMessageNow(const MidiMessage& message) класса MidiOutput, который управляет физическими устройствами MIDI. Для хранения текущего устройства предусмотрим в нашем классе указатель на MidiOutput ( пример 19.7).

Класс MidiOutput позволяет получить список названий доступных устройств MIDI с помощью метода static StringArray MidiOutput::getDevices() ( пример 19.8).

#include "TCentralComponent.h"
//---------------------------------------------------
#define tr(s) String::fromUTF8(s)
//---------------------------------------------------
TCentralComponent::TCentralComponent() : Component("CentralComponent"),
            pMIDIDevicesBox(0),
            pPlayButton(0)
{
  pMIDIDevicesBox = new ComboBox(L"MIDIDevicesBox");
  pMIDIDevicesBox->setEditableText(false);
  pMIDIDevicesBox->setJustificationType(Justification::centredLeft);
  pMIDIDevicesBox->setTextWhenNothingSelected(
            tr("MIDI устройство не выбрано"));
    pMIDIDevicesBox->setTextWhenNoChoicesAvailable(
            tr("Нет доступных устройств"));
  pMIDIDevicesBox->addListener(this);
  addAndMakeVisible(pMIDIDevicesBox);

  pInstrumentsBox = new ComboBox(L"InstrumentsBox");
  pInstrumentsBox->setEditableText(false);
  pInstrumentsBox->setJustificationType(Justification::centredLeft);
  pInstrumentsBox->setTextWhenNothingSelected(
            tr("Нет выбранных инструментов"));
  pInstrumentsBox->addListener(this);
  pInstrumentsBox->setEnabled(false);
  addAndMakeVisible(pInstrumentsBox);

  // Добавляем инструменты в выпадающий список
  AddInstruments();

  pPlayButton = new TextButton(L"PlayButton");
  pPlayButton->setButtonText(tr("Играть"));
  pPlayButton->setColour(TextButton::buttonColourId, Colours::lightgreen);
  pPlayButton->setEnabled(false);
  pPlayButton->addListener(this);
  addAndMakeVisible(pPlayButton);

  // Получаем названия имеющихся устройств MIDI...
  StringArray sMIDIDevices = MidiOutput::getDevices();
  for(int i = 0; i < sMIDIDevices.size(); i++)
  {
  // и добавляем их в список
    pMIDIDevicesBox->addItem(tr("MIDI устройство: ") + sMIDIDevices[i], 
            i + 1);
  }
  pMIDIDevicesBox->setSelectedId(1, false);

  setSize(600, 200);

  // Нота не воспроизводится
  bIsPlaying = false;
  // Инструмент по умолчанию - Acoustic Grand Piano
  iInstrument = 0;
}
Листинг 19.8. Реализация конструктора класса компонента содержимого TCentralComponent (файл TCentralComponent.cpp)

Открыть устройство MIDI с индексом deviceIndex позволяет метод этого же класса static MidiOutput* MidiOutput::openDevice(int deviceIndex). После этого устройству можно посылать сообщения и удалить его, когда оно больше не будет нужно. Индекс устройства представляет собой его позицию в списке, возвращаемом функцией getDevices.

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

В нашем примере открытие устройства MIDI происходит после его выбора пользователем в выпадающем списке ( пример 19.9).

void TCentralComponent::comboBoxChanged(ComboBox* pComboBox)
{
  if(pComboBox == pMIDIDevicesBox)
  {
    // Открываем выбранное пользователем устройство MIDI
    pMidiOutput = MidiOutput::openDevice(
            pMIDIDevicesBox->getSelectedId() - 1);
    // Если нет доступных устройств, выводим сообщение об этом
    if(pMidiOutput == NULL)
    {
      AlertWindow::showMessageBox(
            AlertWindow::WarningIcon, tr("Ошибка"),
            tr("Невозможно открыть MIDI устройство!"), 
            tr("Принять"), 0);
            pPlayButton->setEnabled(false);
      pInstrumentsBox->setEnabled(false);
        }
    else 
    {
      pPlayButton->setEnabled(true);
      // Иначе даём пользователю выбрать инструмент
      pInstrumentsBox->setEnabled(true);
    }
  }
  else if(pComboBox == pInstrumentsBox)
  {
    // Пользователь выбрал инструмент
    iInstrument = pInstrumentsBox->getSelectedId();
  }
}
Листинг 19.9. Реализация метода comboBoxChanged класса компонента содержимого TCentralComponent (файл TCentralComponent.cpp)

Наша программа при нажатии на кнопку pPlayButton будет играть ноту до первой октавы выбранным пользователем инструментом (тембром) вплоть до повторного нажатия на эту же кнопку. В случае, если инструмент не был выбран, по умолчанию будет использоваться Acoustic Grand Piano ( пример 19.8). Цвет кнопки будет меняться при воспроизведении ноты и его остановке.

В нашем примере отправка сообщения Note on реализована в методе void TCentralComponent::Play() ( пример 19.10).

void TCentralComponent::Play()
{
  // Задаём тембр воспроизведения
  pMidiOutput->sendMessageNow(MidiMessage::programChange(1, iInstrument));
  // Команда Note on
  MidiMessage PlayMessage(0x90, 60, 100, 0);
  // Выбираем канал (в нашей программе вызываемый метод ничего не делает)
  PlayMessage.setChannel(1);
  // Отправляем сообщение
  pMidiOutput->sendMessageNow(PlayMessage);
}
Листинг 19.10. Реализация метода Play класса компонента содержимого TCentralComponent (файл TCentralComponent.cpp)

Как уже упоминалось выше, команды MIDI посылаются одному из 16 логических каналов, отвечающих за режим воспроизведения нот тем или иным инструментом. Так, для смены инструмента на первом канале протокол MIDI предусматривает команду 0xC0 NN, на втором - 0xC1 NN и т.д., где NN — номер инструмента. Класс MidiMessage включает универсальный метод смены инструмента на заданном канале static const MidiMessage MidiMessage::programChange(int channel, int programNumber) throw(), где, как понятно, channel — это номер канала, а programNumber — номер инструмента ( пример 19.10).

Задать канал для отправки сообщений MIDI позволяет метод void MidiMessage::setChannel(int newChannelNumber) throw(). Принимаемый параметр должен, разумеется, находиться в пределах от 1 до 16. В нашем примере вызов этого метода не нужен и приведён исключительно в иллюстративных целях. Если мы закомментируем строку PlayMessage.setChannel(1), то никаких изменений в работе нашей программы не произойдёт, поскольку сообщение MidiMessage PlayMessage(0x90, 60, 100, 0) и означает воспроизведение ноты до на первом канале. Для проигрывания её на втором канале конструктор сообщения должен в качестве первого параметра принимать значение 0x91, на третьем — 0x92 и т.п. Но попробуйте изменить параметр метода setChannel с 1 на 10 и пересоберите программу. Нота будет воспроизводиться ударной установкой синтезатора, а выбор инструмента пользователем будет проигнорирован.

Метод Play будет вызываться из обработчика события щелчка по кнопке pPlayButton в том случае, если нота на момент нажатия на кнопку не воспроизводилась ( пример 19.11). Цвет кнопки при этом меняется со светло-зелёного на розовый.

void TCentralComponent::buttonClicked(Button* pButton)
{
  if(pButton == pPlayButton)
  {
    if(bIsPlaying == false)
    {
      Play();
      // Меняем текст...
      pPlayButton->setButtonText(tr("Остановить"));
      // ...и цвет кнопки (новый цвет - розовый)
      pPlayButton->setColour(TextButton::buttonColourId, Colours::pink);
      bIsPlaying = true;
    }
    else
    {
      // Команда Note off
      MidiMessage StopMessage(0x80, 60, 0);
      // Выбираем канал 
      // (в нашей программе вызываемый метод ничего не делает)
      StopMessage.setChannel(1);
      pMidiOutput->sendMessageNow(StopMessage);
      // Меняем текст...
      pPlayButton->setButtonText(tr("Играть"));
      // ...и цвет кнопки (новый цвет - светло-зелёный)
      pPlayButton->setColour(
            TextButton::buttonColourId, 
            Colours::lightgreen);
      bIsPlaying = false;
    }
  }
}
Листинг 19.11. Реализация метода buttonClicked класса компонента содержимого TCentralComponent (файл TCentralComponent.cpp)

В противном случае мы останавливаем воспроизведение, посылая команду Note off — MidiMessage StopMessage(0x80, 60, 0). Как уже упоминалось выше, остановить проигрывание ноты можно, отправив сообщение MIDI о её воспроизведении с нулевой скоростью, например, так:

MidiMessage StopMessage(0x90, 60, 0);
pMidiOutput->sendMessageNow(StopMessage);

Замените в коде нашей программы первый байт сообщения MIDI с 0x80 на 0x90 и пересоберите её. Убедитесь, что работа приложения не изменилась.

Полный исходный текст этого примера вы можете найти на прилагаемом к книге компакт-диске.

< Лекция 18 || Лекция 19: 12345 || Лекция 20 >