Введение


В своих статьях о переходе на российский микроконтроллер К1986ВЕ92QI я ни раз рассказывал о генерации звука средствами микроконтроллера. Тогда передо мной стаяла задача лишь воспроизвести данные. Для создания этих самих данных, получаемых из MIDI файлов, использовались весьма экзотические методы, например, как в этой статье. Да, подобные методы имеют право на жизнь, если требуется получить данные для воспроизведения пару раз в жизни. Но так как я достаточно часто сталкиваюсь с задачами, когда на контроллере нужно получить достаточно сложный звук, или же звук — лишь дополнительная опция, то задача преобразовывать MIDI файлы такими экзотическими способами, становиться весьма нетривиальной. В этой небольшой серии статей я поставил для себя задачу создать (а за одно и подробно рассказать о процессе создания) универсальную программу для преобразования MIDI файлов в приемлемый для микроконтроллера формат, а так же генерирующую все необходимые для микроконтроллера данные инициализации.



Итогом данной статьи станет реализация основного функционала программы: создание массивов нота-длительность, созданного из MIDI файла. Кто заинтересовался — прошу под кат.

Структура статьи


  1. Выработка требований к программе.
  2. Определение способа реализации.
  3. Общие сведения о MIDI.
  4. Заголовок.
  5. Блок MIDI файла.
  6. События.
  7. Разбор полученных данных.
  8. Заключение.


Выработка требований к программе


Как уже говорилось выше, основной задачей нашей программы будет преобразование данных из MIDI формата в наш собственный. Иначе говоря, перед нами не стоит задачи учитывать силу нажатия клавиш, использование редких инструментов или же использования эффектов, предусмотренных стандартом MIDI. Всю эту и подобную ей ненужную информацию мы должны игнорировать. По окончании работы программы, мы должны получить N-е число массивов, в каждом из которых в текущий момент времени будет играть лишь одна клавиша (это нужно для упрощения программы в микроконтроллере). Иначе говоря, мы должны получить список массивов с полифонией в одну ноту.

Определение способа реализации


В одной из предыдущих статей мы уже писали программу, которая реализовала подобный функционал на основе уже переработанных другой программой данных в специфичном виде. Программа была написана на Pascal ABC, потому что на тот момент задача сводилась к обработке строк txt файла, не более. Сейчас же мы пишем программу с нуля, предполагая работу с чистыми данными MIDI. Так же, в будущем планируем расширить ее до полноценного генератора данных инициализации. Так что в этот раз программа будет написана в графической среде Visual Studio на языке C#. Много ресурсов компьютера нам не требуется, а красивый синтаксис и возможность легкого чтения программы, способствующее легкой поддержки — не помешают.

Общие сведения о MIDI


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



Как видно из рисунка, MIDI файл состоит из:
  • Заголовка файла (он начинается с четырех символов, составляющих слово MThd).
  • Блоков файла (начинающихся с символов MTrk).
Для начала давайте рассмотрим заголовок MIDI файла (MThd).

Заголовок


Разберем, из чего состоит заголовок MIDI файла.

  • Стандартные значения. В заголовке присутствуют ячейки, значения которых одинаковы для всех MIDI файлов.
    1. Надпись заголовка «MThd». Данный параметр позволяет однозначно сказать, что перед нами блок заголовка.
    2. Размер индивидуальных параметров файла в блоке заголовка. Так как в заголовке всегда присутствуют 3 индивидуальных параметра, каждый из которых занимает по 2 байта — то общая длинна блока заголовка (без учета надписи «MThd» и четырех байт самого размера) составляет 6 байт.
  • Индивидуальные параметры.
    1. Формат MIDI файла. По сути говоря, форматов MIDI файла всего 2: 0 и 1. Имеется еще формат 2, но за всю свою девятилетнюю работу со звуком, в реальной жизни мне так и не довелось столкнуться с MIDI файлом в этом формате. Данный параметр показывает, как упакованы события (в нашем случае, нажатия/отпускание клавиш). Если перед нами формат 0, то мы знаем наверняка, что вся полезная информация обо всех каналах (коих может быть до 16) расположена в одном единственном блоке MTrk. Если же перед нами формат 1, то каждый канал имеет свой собственный блок MTrk. Наша программа будет иметь возможность работать с обоими форматами.
    2. Число блоков MIDI файла (MTrk). Тут мы можем посмотреть, сколько блоков содержится в нашем MIDI файле. Данный параметр актуален лишь для формата 1. Ибо в формате 0 блок всегда 1.
    3. Формат времени MIDI файла. А вот тут дела обстоят весьма интересно. Дело в том, что в MIDI файле счет идет не секундами, а «тиками». Причем существует музыкальный способ, когда значение нашего параметра показывает, сколько «тиков» приходится на музыкальную четверть и абсолютный, показывающий количество «тиков» в SMPTE блоке. Опять же. Чаще всего встречается первый способ. Второй, все таки, экзотика. Поэтому мы не будем учитывать существования абсолютного способа отсчета времени и будем оперировать только музыкальным.

Теперь, зная структуру заголовка MIDI файла, мы можем его считать. Но прежде нужно уяснить один момент. Данные в MIDI файле (длина которых более одного байта), представлены в формате big-endian. Это значит, что если перед нами ячейка, состоящая из двух байт, то первым байтом идет старший байт, а вторым — младший. Непривычно, но формат не молодой, и можно ему это простить.

Итак, считываем заголовок.

  1. Для работы нам нужно создать Windows Forms приложение (WPF тут без надобности, но если хотите, то никто не запрещает).
  2. Внутри формы создадим button и richTextBox (у меня они имеют имена button1 и richTextBox1 соответственно), а так же окно для открытия файла openFileDialog (у меня, опять же, имеет имя openFileDialogMIDI).
  3. Создадим событие, привязанное к нажатию на кнопку, в котором очистим richTextBox от старых данных. Так же получим путь к MIDI файлу и передадим его функции, которая откроет его. (openMIDIFile)
    Код события.
    private void button1_Click(object sender, EventArgs e)
            {
                richTextBox1.Clear();
                if (openFileDialogMIDI.ShowDialog() == DialogResult.OK)         // Если диалоговое окно нормально открылось.
                {
                    openMIDIFile(openFileDialogMIDI.FileName);                  // Открываем файл для чтения.
                }
            }
  4. Так как MIDI файл имеет необычный формат представления данных (big-endian), то будет проще создать класс, в котором мы бы определили методы, для комфортной работы с MIDI файлом.
    Код метода создания собственного потока работы с MIDI фалом.
    // Класс для работы с файловым потоком файла MIDI.
        public class MIDIReaderFile 
        {
            public BinaryReader BinaryReaderMIDIFile;   // Создаем поток. На его основе будем работать с MIDI файлом.
            public MIDIReaderFile(Stream input) // В конструкторе инициализируем байтовый поток на основе открытого потока.
            {
                BinaryReaderMIDIFile = new BinaryReader(input); // Открываем поток для чтения по байтам на основе открытого потока файла.
            }
    
            public UInt32 ReadUInt32BigEndian() // Считываем 4 байта в формате "от старшего к младшему" и располагаем их в переменной.
            {
                UInt32 bufferData = 0;  // Начальное значени = 0.
                for (int IndexByte = 3; IndexByte >= 0; IndexByte--)    // Счетчик от старшего к младшему.
                    bufferData |= (UInt32)((UInt32)BinaryReaderMIDIFile.ReadByte()) << 8 * IndexByte;   // Располагаем значения. 
                return bufferData;
            }
    
            public UInt16 ReadUInt16BigEndian() // Считываем 2 байта в формате "от старшего к младшему" и располагаем их в переменной.
            {
                UInt16 bufferData = 0;  // Начальное значени = 0.
                for (int IndexByte = 1; IndexByte >= 0; IndexByte--)    // Счетчик от старшего к младшему.
                    bufferData |= (UInt16)((UInt16)BinaryReaderMIDIFile.ReadByte() << 8 * IndexByte);   // Располагаем значения. 
                return bufferData;
            }
    
            public string ReadStringOf4byte()   // Получаем из файла строку в 4 элемента.
            {
                return Encoding.Default.GetString(BinaryReaderMIDIFile.ReadBytes(4));   // Достаем 4 байта и преобразовываем их в стоку из 4-х символов.
            }
    
            public byte ReadByte()  // Считываем 1 байт.
            {
                return BinaryReaderMIDIFile.ReadByte();
            }
    
            public byte[] ReadBytes(int count)  // Считываем count байт.
            {
                return BinaryReaderMIDIFile.ReadBytes(count);
            }
        }

  5. Далее создадим структуру, в которой будем хранить данные MIDI.
    Структура MThd блока.
    // Назначение: Хранить параметры заголовка MIDI файла.
    // Применение: Структура создается при первом чтении MIDI файла.
            public struct MIDIheaderStruct
            {
                public string nameSection; // Имя раздела. Должно быть "MThd".
                public UInt32 lengthSection; // Длинна блока, 4 байта. Должно быть 0x6;
                public UInt16 mode; // Режим MIDI файла: 0, 1 или 2. 
                public UInt16 channels; // Количество каналов. 
                public UInt16 settingTime;  // Параметры тактирования.
            }
  6. Теперь создадим метод, который будет считывать из потока нашу структуру и возвращать ее.
    Считывание заголовка.
    // Назначение: разбор главной структуры MIDI файла.
    // Параметры: Открытый FileStream поток.
    // Возвращаемой значение - заполненная структура типа MIDIheaderStruct.
            public MIDIheaderStruct CopyHeaderOfMIDIFile(MIDIReaderFile MIDIFile)
            {
                MIDIheaderStruct ST = new MIDIheaderStruct(); // Создаем пустую структуру заголовка файла.
                ST.nameSection      = MIDIFile.ReadStringOf4byte(); // Копируем имя раздела. 
                ST.lengthSection    = MIDIFile.ReadUInt32BigEndian(); // Считываем 4 байта длины блока. Должно в итоге быть 0x6
                ST.mode             = MIDIFile.ReadUInt16BigEndian(); // Считываем 2 байта режима MIDI. Должно быть 0, 1 или 2.
                ST.channels         = MIDIFile.ReadUInt16BigEndian(); // Считываем 2 байта количество каналов в MIDI файле. 
                ST.settingTime      = MIDIFile.ReadUInt16BigEndian(); // Считываем 2 байта параметров тактирования.
                return ST; // Возвращаем заполненную структуру.
            }
  7. Теперь напишем функцию, которую вызывает событие нажатия кнопки открытия файла. Данную функцию мы еще будем дополнять. А пока что ее основная задача открыть файл и считав его значения, вывести полученные индивидуальные параметры.
    Код функции открытия файла.
    // Назначение: Открытие файла для чтения.
    // Параметры: путь к файлу.
    // Возвращаемое значение: успешность операции. true - успешно, false - нет.
            public bool openMIDIFile(string pathToFile)
            {
                FileStream fileStream = new FileStream(pathToFile, FileMode.Open, FileAccess.Read); // Открываем файл только для чтения.
                MIDIReaderFile MIDIFile = new MIDIReaderFile(fileStream); // Собственный поток для работы с MIDI файлом со спец. функциями. На основе байтового потока открытого файла.
                MIDIheaderStruct HeaderMIDIStruct = CopyHeaderOfMIDIFile(MIDIFile); // Считываем заголовок.
                MIDIMTrkStruct[] MTrkStruct = new MIDIMTrkStruct[HeaderMIDIStruct.channels]; // Определяем массив для MTrkStruct.
                richTextBox1.Text += "Количество блоков: " + HeaderMIDIStruct.channels.ToString() + "\n"; // Количество каналов.
                richTextBox1.Text += "Параметры времени: " + HeaderMIDIStruct.settingTime.ToString() + "\n";
                richTextBox1.Text += "Формат MIDI: " + HeaderMIDIStruct.mode.ToString() + "\n";     
                return true;
            }
Прошу обратить внимание на строку создания массив структур MTrkStruct. Как говорилось выше, в заголовке файла есть ячейка, указывающая, сколько еще блоков, помимо блока заголовка, содержится в MIDI файле. Сразу же после считывания заголовка мы можем создать массив структур информационных блоков MIDI файла. Данная структура будет рассмотрена далее. После выбора MIDI файла, мы увидим следующее.

Блок MIDI файла


Рассмотрев заголовок файла, мы можем приступить к рассмотрению структуры блока.



Блок состоит из:
  1. Четырех символов, составляющих слово MTrk. Это указатель того, что перед нами MIDI блок.
  2. Длинны блока, записанной с помощью четырех байт. В длину блока не входят первые 8 байт (MTrk + 4 байта длины).

События.

Вот мы и подошли к самому интересному. Именно в событиях содержится вся нужная нам информация. MIDI события бывают четырех типов.
  1. События первого уровня.



    В MIDI файлах принято считать, что существуют 16 каналов. Место, где находится номер канала помечено как nnnn. 0 = первому каналу, 1 = второму и так далее. Таким образом, под номер канала выделены младшие 4 бита. На каждом канале может быть нажато N-е число нот. В зависимости от того, сколько позволяет воспроизвести устройство, читающее MIDI файл. Номер канала для нас не имеет никакой роли, потому что у нас в тех. задании ясно сказано, что на каждом канале в текущий момент времени должно быть включено не более одной клавиши. Иначе говоря, разбитие по каналам мы будем осуществлять сами. Из представленных команд первого уровня мы будем использовать 0x8n (отпустить ноту), 0x9n (взять ноту), 0xBn (для обращения к сообщения второго уровня, о чем будет далее) и 0xA (сменить силу нажатия клавиши).
  2. События второго уровня. Данные события представляют из себя событие первого уровня 0xBn + номер события (коих порядка сотни) + параметр данного события (если параметра нет, то передается 0).



    Команды второго уровня мы использовать не будем. Но мы теперь знаем, как их игнорировать.
  3. События третьего уровня. События третьего уровня представляют из себя 3 события второго. Первыми двумя событиями мы указываем номер нужной команды, а в третей — ее параметр.



    Команды третьего уровня мы так же не используем. А метод их игнорирования совпадает с методом игнорирования команд второго уровня (по сути ведь мы игнорируем 3 команды второго).
  4. SysEx-события. Это эксклюзивные сообщения. В MIDI файлах партитур фортепианных произведений (или других классических инструментов) не встречается. При написании программы мы будем считать, что таковых сообщений не существует. Структура сообщения выглядит так.



Теперь, зная о том, какие события существуют мы бы могли приступить к их считыванию, но… В какой момент времени они появляются? А вот тут все обстоит следующим образом. Перед каждым событием первого/второго уровня (третий не рассматриваем, так как мне за все время тестирования нотных произведений еще ни разу не попалось такого MIDI файла) стоит n-е количество байт, описывающих прошедшее с последнего MIDI события время. Если байт данных о времени последний, то его старший байт установлен в 0. Если нет, то 1. Рассмотрим пример.



Флаг установлен в 0 (7-й бит = 0). Следовательно этот байт последний и единственный. Далее, не обращая внимания на старший разряд, смотрим на оставшееся число. Оно равно 0 => событие 0 произошло в нулевую секунду. Теперь рассмотрим событие 1. Тут уже старший байт установлен в 1 => байт не последний. Сохраняем значение оставшееся, если вычеркнуть старший разряд. Получаем 1. Смотрим следующий байт. Там флаг = 0 и оставшаяся часть = 0. Теперь считаем, сколько на самом деле прошло времени. Так как каждый байт может переносить всего лишь по 7 байт информации, то у нас прошло (1<<7)|(0<<0) = 0x100 тиков таймера. Точно так же можно рассмотреть и время перед событием 2. Там у нас прошло 0x10 тиков.

Стоит заметить, что если событием 0 является, например, команда взять клавишу, событие 1 мы игнорируем, а событие 2 является командой отпустить клавишу, то нам нужно обязательно учесть, что время нажатия на клавишу = 0x100 + 0x10. Ибо отсчет идет от последнего события. Даже если мы его игнорируем.

Вооружившись всей полученной информацией мы можем приняться писать код.
  • Создадим структуру, в которой будем хранить заголовок блока, а так же список со считанными нотами.
    Структура блока
    // Назначение: Хранить блок с событиями MIDI блока.
    // Применение: Создается перед чтением очередного блока MIDI файла.
            public struct MIDIMTrkStruct
            {
                public string nameSection; // Имя раздела. Должно быть "MTrk".
                public UInt32 lengthSection; // Длинна блока, 4 байта.
                public ArrayList arrayNoteStruct; // Динамический массив с нотами и их изменениями.
            }


  • Так же требуется структура, чтобы хранить считанные события нажатия/отпускания/изменения силы нажатия клавиши.
    Структура ноты.
    // Назначение: хранить события нажатия/отпускания клавиши или смены ее громкости.
            public struct noteStruct
            {
                public byte     roomNotes;                    // Номер ноты.
                public UInt32   noteTime;                     // Длительность ноты время обсалютное. 
                public byte     dynamicsNote;                 // Динамика взятия/отпускания ноты.
                public byte     channelNote;                  // Канал ноты.
                public bool     flagNote;                     // Взятие ноты (true) или отпускание ноты (false).    
            }


  • Теперь напишем функцию, которая считывает данные, определяет, нужны ли они для создания массива и складывает нужные в массив, с указанием реального времени от нуля.
    Функция считывания блока.
    // Назначение: копирование блока MTrk (блок с событиями) из MIDI файла.
    // Параметры: поток для чтения MIDI файла.
    // Возвращает: структуру блока с массивом структур событий.
            public MIDIMTrkStruct CopyMIDIMTrkSection(MIDIReaderFile MIDIFile)
            {
                MIDIMTrkStruct ST = new MIDIMTrkStruct(); // Создаем пустую структуру блока MIDI файла. 
                ST.arrayNoteStruct = new ArrayList(); // Создаем в структуре блока динамический массив структур событий клавиш.
                noteStruct bufferSTNote = new noteStruct(); // Создаем запись о новой ноте (буферная структура, будем класть ее в arrayNoteStruct).
                ST.nameSection      = MIDIFile.ReadStringOf4byte(); // Копируем имя раздела. 
                ST.lengthSection    = MIDIFile.ReadUInt32BigEndian(); // 4 байта длинны всего блока.
                UInt32 LoopIndex    = ST.lengthSection; // Копируем колличество оставшихся ячеек. Будем считывать события, пока счетчик не будет = 0.
                UInt32 realTime = 0; // Реальное время внутри блока.
                while (LoopIndex != 0) // Пока не считаем все события.
                {
                    // Время описывается плавающим числом байт. Конечный байт не имеет 8-го разрядка справа (самого старшего).
                    byte loopСount = 0; // Колличество считанных байт.
                    byte buffer; // Сюда кладем считанное значение.
                    UInt32 bufferTime = 0; // Считанное время помещаем сюда.                              
                    do {
                        buffer = MIDIFile.ReadByte(); // Читаем значение.
                        loopСount++; // Показываем, что считали байт.
                        bufferTime <<=  7; // Сдвигаем на 7 байт влево существующее значенеи времени (Т.к. 1 старший байт не используется).
                        bufferTime |= (byte)(buffer & (0x7F)); // На сдвинутый участок накладываем существующее время.
                    } while ((buffer & (1<<7)) != 0); // Выходим, как только прочитаем последний байт времени (старший бит = 0).
                    realTime += bufferTime; // Получаем реальное время.
    
                    buffer = MIDIFile.ReadByte(); loopСount++; // Считываем статус-байт, показываем, что считали байт. 
                    // Если у нас мета-события, то...
                    if (buffer == 0xFF)                                         
                    {
                        buffer = MIDIFile.ReadByte(); // Считываем номер мета-события.
                        buffer = MIDIFile.ReadByte(); // Считываем длину.
                        loopСount+=2;
                        for (int loop = 0; loop < buffer; loop++)
                            MIDIFile.ReadByte();
                        LoopIndex = LoopIndex - loopСount - buffer; // Отнимаем от счетчика длинну считанного.   
                    } 
      
                    // Если не мета-событие, то смотрим, является ли событие событием первого уровня.
                    else switch ((byte)buffer & 0xF0) // Смотрим по старшым 4-м байтам.
                    {
                        // Перебираем события первого уровня.
                       
                        case 0x80: // Снять клавишу.
                            bufferSTNote.channelNote = (byte)(buffer & 0x0F); // Копируем номер канала.
                            bufferSTNote.flagNote = false; // Мы отпускаем клавишу.
                            bufferSTNote.roomNotes = MIDIFile.ReadByte(); // Копируем номер ноты.
                            bufferSTNote.dynamicsNote = MIDIFile.ReadByte(); // Копируем динамику ноты.
                            bufferSTNote.noteTime = realTime; // Присваеваем реальное время ноты.
                            ST.arrayNoteStruct.Add(bufferSTNote); // Сохраняем новую структуру.
                            LoopIndex = LoopIndex - loopСount - 2; // Отнимаем прочитанное. 
                            break;
                        case 0x90:   // Нажать клавишу.
                            bufferSTNote.channelNote = (byte)(buffer & 0x0F); // Копируем номер канала.
                            bufferSTNote.flagNote = true; // Мы нажимаем.
                            bufferSTNote.roomNotes = MIDIFile.ReadByte(); // Копируем номер ноты.
                            bufferSTNote.dynamicsNote = MIDIFile.ReadByte(); // Копируем динамику ноты.
                            bufferSTNote.noteTime = realTime; // Присваеваем реальное время ноты.
                            ST.arrayNoteStruct.Add(bufferSTNote); // Сохраняем новую структуру.
                            LoopIndex = LoopIndex - loopСount - 2; // Отнимаем прочитанное. 
                            break;
                        case 0xA0:  // Сменить силу нажатия клавишы. 
                            bufferSTNote.channelNote = (byte)(buffer & 0x0F); // Копируем номер канала.
                            bufferSTNote.flagNote = true; // Мы нажимаем.
                            bufferSTNote.roomNotes = MIDIFile.ReadByte(); // Копируем номер ноты.
                            bufferSTNote.dynamicsNote = MIDIFile.ReadByte(); // Копируем НОВУЮ динамику ноты.
                            bufferSTNote.noteTime = realTime; // Присваеваем реальное время ноты.
                            ST.arrayNoteStruct.Add(bufferSTNote); // Сохраняем новую структуру.
                            LoopIndex = LoopIndex - loopСount - 2; // Отнимаем прочитанное.     
                            break;
                        // Если 2-х байтовая комманда.
                        case 0xB0:  byte buffer2level = MIDIFile.ReadByte(); // Читаем саму команду.
                                    switch (buffer2level) // Смотрим команды второго уровня.
                                    {
                                        default: // Для определения новых комманд (не описаных).
                                            MIDIFile.ReadByte(); // Считываем параметр какой-то неизвестной функции.
                                            LoopIndex = LoopIndex - loopСount - 2; // Отнимаем прочитанное. 
                                            break;                                              
                                    }
                                    break;
                       
                        // В случае попадания их просто нужно считать.
                        case 0xC0:   // Просто считываем байт номера.
                            MIDIFile.ReadByte(); // Считываем номер программы.
                            LoopIndex = LoopIndex - loopСount - 1; // Отнимаем прочитанное. 
                            break;
                       
                        case 0xD0:   // Сила канала.
                            MIDIFile.ReadByte(); // Считываем номер программы.
                            LoopIndex = LoopIndex - loopСount - 1; // Отнимаем прочитанное. 
                            break;
                       
                        case 0xE0:  // Вращения звуковысотного колеса.
                            MIDIFile.ReadBytes(2); // Считываем номер программы.
                            LoopIndex = LoopIndex - loopСount - 2; // Отнимаем прочитанное. 
                            break;
                    }
                }
                return ST; // Возвращаем заполненную структуру.
            }
  • Но считать ноты недостаточно. Как говорилось выше, нам нужно, чтобы на каждом канале в момент времени играла лишь одна нота. Значит теперь нам нужно разбить все имеющиеся ноты на необходимое минимальное число каналов.
    Для этого была написана следующая функция.
    // Назначение: создавать список: нота/длительность.
            // Параметры: массив структур блоков, каждый из которых содержит массив структур событий; количество блоков.
            public ArrayList СreateNotesArray(MIDIMTrkStruct[] arrayST, int arrayCount)
            {
                ArrayList arrayChannelNote = new ArrayList(); // Массив каналов.
    
                for (int indexBlock = 0; indexBlock < arrayCount; indexBlock++) // Проходим по всем блокам MIDI.
                {
                    for (int eventArray = 0; eventArray < arrayST[indexBlock].arrayNoteStruct.Count; eventArray++) // Пробегаемся по всем событиям массива каждого канала.
                    {
                        noteStruct bufferNoteST = (noteStruct)arrayST[indexBlock].arrayNoteStruct[eventArray]; // Достаем событие ноты.
                        if (bufferNoteST.flagNote == true) // Если нажимают ноту.
                        {
                            byte indexChennelNoteWrite = 0;
                            while (true) // Перебераем каналы для записи.
                            {
                                if (indexChennelNoteWrite<arrayChannelNote.Count) // Если мы еще не просмотрели все существующие каналы.
                                {
                                    channelNote bufferChannel = (channelNote)arrayChannelNote[indexChennelNoteWrite]; // Достаем канал с выбранным номером.
    
                                    if (bufferChannel.ToWriteaNewNote(bufferNoteST.roomNotes, bufferNoteST.noteTime) == true) break; // Если запись проша удачно - выходим.
                                }
                                else // Если свободного канала не найдено - создать новый и кинуть в него все.
                                {
                                    channelNote noteNambeChannelBuffer = new channelNote(); // Канал с реальным временем предыдущего.
                                    noteNambeChannelBuffer.ToWriteaNewNote(bufferNoteST.roomNotes, bufferNoteST.noteTime);// Если запись проша удачно - выходим.
                                    arrayChannelNote.Add(noteNambeChannelBuffer); // Добавляем канал в массив каналов.
                                    break;  // Наверняка выходим.
                                }
                                indexChennelNoteWrite++; // Если не удалось записать - следующий канал.
                            }
                        }
                        else // Если ноту наоборот отпускают.
                        {
                            byte indexChennelNoteWrite = 0;
                            while (true) // Перебераем каналы для записи.
                            {
                                    channelNote bufferChannel = (channelNote)arrayChannelNote[indexChennelNoteWrite]; // Достаем канал с выбранным номером.
                                    if (bufferChannel.EntryEndNotes(bufferNoteST.roomNotes, bufferNoteST.noteTime) == true) break;// Если запись проша удачно - выходим.
                                    indexChennelNoteWrite++; // Если не удалось записать - следующий канал.
                            }
                        }
                    }
                }
                return arrayChannelNote;
            }
  • Предпоследним шагом будет вывод информации в richTextBox.
    Функция вывода.
    // Вывод массивов каналов в richTextBox1.
            public void outData(ArrayList Data)
            {
                for (int loop = 0; loop<Data.Count; loop++) // Идем по всем каналам.
                {
                    channelNote buffer = (channelNote)Data[loop]; // Получаем ссылку на канал.
                    // Проходимся по всем нотам канала.
                    richTextBox1.Text += "uint16_t channel" + loop.ToString() + "[" + buffer.arrayNoteChannel.Count.ToString() + "][2] = {";
                    for (int loop1 = 0; loop1 < buffer.arrayNoteChannel.Count; loop1++)
                    {
                        channelNote.noteInChannelNote DataD = (channelNote.noteInChannelNote)buffer.arrayNoteChannel[loop1];
                        richTextBox1.Text += DataD.roomNotes.ToString() + "," + DataD.noteTime.ToString();
                        if (loop1 != (buffer.arrayNoteChannel.Count - 1)) richTextBox1.Text += ", \t";
                    }
                    richTextBox1.Text += "};\n\n";
                }
            }
  • Ну и нам осталось лишь собрать все эти функция воедино в методе открытия файла. Выглядеть он будет следующим образом.
    Метод открытия MIDI файла.
    // Назначение: Открытие файла для чтения. 
            // Параметры: путь к файлу.
            // Возвращаемое значение: успешность операции. true - успешно, false - нет.
            public bool openMIDIFile(string pathToFile)
            {
                FileStream fileStream = new FileStream(pathToFile, FileMode.Open, FileAccess.Read);  // Открываем файл только для чтения.
                MIDIReaderFile MIDIFile = new MIDIReaderFile(fileStream);                            // Собственный поток для работы с MIDI файлом со спец. функциями. На основе байтового потока открытого файла.
                MIDIheaderStruct HeaderMIDIStruct = CopyHeaderOfMIDIFile(MIDIFile);                  // Считываем заголовок.
                MIDIMTrkStruct[] MTrkStruct = new MIDIMTrkStruct[HeaderMIDIStruct.channels];         // Определяем массив для MTrkStruct.
                richTextBox1.Text += "Количество блоков: " + HeaderMIDIStruct.channels.ToString() + "\n"; // Количество каналов.
                richTextBox1.Text += "Параметры времени: " + HeaderMIDIStruct.settingTime.ToString() + "\n";
                richTextBox1.Text += "Формат MIDI: " + HeaderMIDIStruct.mode.ToString() + "\n";     
                for (int loop = 0; loop<HeaderMIDIStruct.channels; loop++)
                    MTrkStruct[loop] = CopyMIDIMTrkSection(MIDIFile);                                // Читаем блоки MIDI файла.
                outData(СreateNotesArray(MTrkStruct, HeaderMIDIStruct.channels));                    // Получаем список нота/длительность.
                return true;
            }

Заключение


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

Используемые источники


В освоении MIDI очень помогла эта серия статей, писавшаяся ни один год еще с 2003-го года.

Комментарии (7)


  1. Vendict
    27.11.2015 10:18
    +2

    Я для AVR использовал вот эту статью и ПО, которое там выложено:
    Midi Player для AVR
    Там достаточно хорошо проработано, с выбором каналов, примером как будет слышно в реале, с различными вариантами усилителей, различными способами модуляции сигнала. Рекомендую, уже две поделки реализовал.


    1. Vadimatorikda
      27.11.2015 14:23

      Спасибо за ссылку. Посмотрел. Интересно. Очень. Но я все таки закончу начатое и создам собственную программу. Постараюсь реализовать больший функционал, чем там. Своей ссылкой только подогрели интерес) Спасибо.


  1. boeing777
    27.11.2015 12:10

    В свое время тоже пришлось разбираться с MIDI. Использовал вот эту статью: www.muzoborudovanie.ru/articles/midi/midi5.php. Стоит отметить, что формат довольно изощренный в силу своей древности: во-первых, big-endian, и, во-вторых, величины переменной длины с 7-битными байтами :) Кстати, что-то не нашел у Вас упоминания про важные мета-события, такие как end-of-track, set tempo и key signature. Они не имеют значения в рамках решаемой задачи?
    Вообще спасибо за статью, особенно за указание моментов, которые редко встречаются либо вообще не применяются.


    1. Vadimatorikda
      27.11.2015 14:19

      Спасибо за положительный отзыв.

      1. Стоит отметить, что формат довольно изощренный в силу своей древности: во-первых, big-endian, и, во-вторых, величины переменной длины с 7-битными байтами.
        Я отметил это в статье. Да, это по-началу смущало. Да и не только это… Но описав все тонкости извлечения MIDI в отдельном классе — мы избавили себя от надобности помнить их.
      2. Кстати, что-то не нашел у Вас упоминания про важные мета-события, такие как end-of-track, set tempo и key signature
        По-поводу end-of-track. Данное событие содержит ту же структуру, что и другие мета-события. Так же: время, 0xFF, 0x2F, длинна 0. Так как данной команды нет в списке распознаваемых — мы ее просто считываем как мета-событие (3 байта, включая 0xFF). А учитывая тот факт, что после каждого считанного события мы сверяемся со счетчиком байт блока, то после прочтения данного события счетчик как раз будет равен нулю и мы выйдем из цикла чтения блока.
      3. В свое время тоже пришлось разбираться с MIDI. Использовал вот эту статью: www.muzoborudovanie.ru/articles/midi/midi5.php.
        Спасибо за ссылку, но данная серия статей уже имеется в последнем абзаце (в пункте «Используемые источники»).
      4. set tempo и key signature
        Данные мата-события, как правило, не встречаются в простых нотных произведениях. Но конкретно set tempo иногда встречается и задает тот же темп, который и так указан в заголовке MIDI файла. Пока что не было глюков со сменой темпа в процессе. Впоследствии, конечно, расширю программу и добавлю данные пункты. Как вы могли заметить, я еще не учел команду второго уровня «сбросить все ноты на канале». В подобных MIDI файлах так же редко встречается. Но в будущем обязательно планирую добавить (с данной командой довольно много заморочек, а данная программа будет в итоге лишь основой для следующей статьи о российских микроконтроллерах).
      Так же хочу сказать, что в оркестровых произведения «эксклюзивные мета-события» все же используют, так что я обязательно добавлю их игнорирование.


  1. chelaxe
    27.11.2015 14:30

    Мы с детьми как то делали проект для изучения азбуки морзе. Тоже пришлось повозится с генерацией midi файлов. Вот может кому будет интересно.


  1. Disasm
    27.11.2015 19:15
    +1

    Сложным путём вы пошли. Зачем использовать midi, когда есть xm? Он намного проще, можно просто побайтово зашивать в код и парсить на лету.


    1. Vadimatorikda
      28.11.2015 05:02
      +1

      О данном формате слышу впервые. Но погуглил. Интересно. Но, опять же, редкий. Цель у меня, все же, снизить нагрузку на МК как можно сильнее. Т.к. воспроизведение звука может быть не основной задачей. Соответственно нужно еще более простой формат чем xm. О написании легковесного и быстрого драйвера на МК я напишу еще серию статеек (уже начала, можете посмотреть).