среда, 6 июня 2018 г.

MIDI музыка на C#. Часть 1-я.

  1. Воспроизводим ноту
  2. Сыграем мелодию
  3. Воспользуемся старыми наработками
Разбираясь в вопросах темы "Мелодии на бипере", решил не останавливаться на достигнутом и пошел дальше. Наиболее естественным способом что-то сыграть на компьютере является использование MIDI. Естественно, для разработки профессионального музыкального софта пришлось бы изучать все спецификации и разбираться во множестве технических моментов. Но для того, чтобы написать что-то небольшое и несложное, достаточно иметь базовые представления о том, как это все работает. Вот в этом мы сейчас и будем разбираться.

Воспроизводим ноту



В деталях этот вопрос описан в статье Making Music with MIDI and C#, так что пересказывать подробно здесь все не буду, опишу тезисно только то, что понадобится для дальнейшего развития темы.

Для реализации задуманного нам понадобится три WinAPI функции
Код csharp Выделить
using System.Runtime.InteropServices;
Код csharp Выделить
        [DllImport("winmm.dll")]
        private static extern int midiOutOpen(ref int handle,
   int deviceID, MidiCallBack proc, int instance, int flags);
 
        [DllImport("winmm.dll")]
        protected static extern int midiOutShortMsg(int handle,
           int message);
 
        [DllImport("winmm.dll")]
        protected static extern int midiOutClose(int handle);
С помощью первой мы будем открывать MIDI-устройство и получать его дескриптор (hadle), с помощью последнего - закрывать, а функция midiOutShortMsg - это как раз и есть основная функция с помощью которой и будет воспроизводиться музыка.
Простейший код, который позволит нам воспроизвести ноту до первой октавы будет выглядеть так
Код csharp Выделить
int handle = 0; // Переменная для хранения дескриптора устройства
res = midiOutOpen(ref handle, 0, null, 0, 0); // Открываем устройство  и получаем его дескриптор
res = midiOutShortMsg(handle, 0x007F3C90); // Воспроизводим ноту
Функция midiOutShortMsg может выполнять огромное количество всевозможных операций и что именно будет делать эта функция - зависит от значения второго аргумента. В общем и целом она посылает устройству с дескриптором, передаваемым первым аргументом сообщение - второй аргумент.

Теперь немного о сообщении. Хоть оно и имеет тип int, тем не менее оно состоит из трех частей, по одному байту каждая.
  1. Первый байт (читаем справа-налево, то есть в сообщении 0x007F3C90 это будут шестнадцатиричные цифры 90) - это так называемый статус-байт, он содержит команду и номер канала. В данном случае команда - 9 означает взятие ноты NoteOn. Она будет воспроизведена на первом канале. Если надо воспроизвести ноту на втором канале мы будем использовать команду 91 и так далее, всего шестнадцать каналов.
  2. Второй байт( в данном случае 3C) - собственно нота, которую мы хотим воспроизвести. Все ноты пронумерованы от 0 до 127. Таблица нот есть в статье, упоминавшейся вначале.
  3. Ну и третий байт - сила с которой взята нота. Обычно этот параметр влияет на громкость, но если речь идет, например, о фортепианной ноте, то это будет сила удара и т.д. (в конечном итоге все равно будет громче). Эта величина также может принимать значения от 0 до 127.

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

Сыграем мелодию



Казалось бы, вот теперь выдавай себе команды на взятие клавиш и получай мелодию, как это было на бипере. Единственное, что непонятно, так это как долго будет звучать взятая нота, прежде чем будет взята следующая. Но на самом деле, если мы захотим проиграть последовательно ноты до, ми и соль первой октавы (60, 64 и 67 соответственно) и для этого выполним вот такой код
Код csharp Выделить
int handle = 0; 
res = midiOutOpen(ref handle, 0, null, 0, 0);
midiOutShortMsg(handle, 127 << 16 | 60 << 8 | 0x00000090);
midiOutShortMsg(handle, 127 << 16 | 64 << 8 | 0x00000090);
midiOutShortMsg(handle, 127 << 16 | 67 << 8 | 0x00000090);
То эти ноты воспроизведутся одновременно и мы услышим аккорд до-мажор. Продолжительность же звучания будет сколь угодно долгой. Хотя на некоторых инструментах звучание скоро прекратится, но это связано с тем, что они имитируют инструменты с угасающими колебаниями. Например гитарный или фортепианный звуки со временем угасают из-за того, что струны, которые их воспроизводят просто перестают колебаться. А вот если выбрать инструмент типа органа, то взятая нота будет звучать пока ее эту вакханалию не прекратишь тем или иным способом.

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

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

Команда NoteOff задается как 8x, в отличие от 9x для NoteOn. Здесь также вместо x нужно указать канал на котором надо прекратить звучание ноты, то есть для первого канала это будет 80. И таким образом код, проигрывающий три ноты подряд будет выглядеть так.
Код csharp Выделить
midiOutShortMsg(handle, 127 << 16 | 60 << 8 | 0x00000090);
Thread.Sleep(500);
midiOutShortMsg(handle,  60 << 8 | 0x00000080);
midiOutShortMsg(handle, 127 << 16 | 64 << 8 | 0x00000090);
Thread.Sleep(500);
midiOutShortMsg(handle,  64 << 8 | 0x00000080);
midiOutShortMsg(handle, 127 << 16 | 67 << 8 | 0x00000090);
Thread.Sleep(500);
midiOutShortMsg(handle,  67 << 8 | 0x00000080);
Thread.Sleep(500);
midiOutClose(handle);
В конце я закрыл устройство. Это необходимо делать, поскольку, если оно остается открытым, то открыть его снова нельзя и надо будет либо использовать ранее полученный дескриптор, либо устройство будет закрыто только вместе приложением, открывшим его. В предыдущих примерах я этого не делал из-за того, что, закрыв устройство сразу после открытия, мы бы ничего не услышали. Здесь же выдержаны паузы и устройство закрывается своевременно.

Воспользуемся старыми наработками



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

Создадим небольшой плеер для воспроизведения мелодий.
Код csharp Выделить
using System.Runtime.InteropServices;
using System.Threading;
 
namespace midi
{
    class MelodyPlayer
    {
 
        [DllImport("winmm.dll")]
        private static extern int midiOutOpen(ref int handle,
   int deviceID, MidiCallBack proc, int instance, int flags);
 
        [DllImport("winmm.dll")]
        protected static extern int midiOutShortMsg(int handle,
           int message);
 
        [DllImport("winmm.dll")]
        protected static extern int midiOutClose(int handle);
 
        private delegate void MidiCallBack(int handle, int msg,
   int instance, int param1, int param2);
 
 
        int handle = 0;
        public void Open()
        {
            var res = midiOutOpen(ref handle, 0, null, 0, 0);
        }
 
        public void Close()
        {
            var res = midiOutClose(handle);
        }
 
        public void SetInstrument(int id)
        {
            var res = midiOutShortMsg(handle, id << 8 | 0x000000C0);
        }
 
        public void PlayNote(int note, int duration)
        {
            var res = midiOutShortMsg(handle, 127 << 16 | note << 8 | 0x00000090);
            Thread.Sleep(duration);
            res = midiOutShortMsg(handle, 127 << 16 | note << 8 | 0x00000080);
        }
    }
}
Здесь у нас есть метод Open, открывающий устройство 0. Метод Close, закрывающий открытое устройство. Метод SetInstrument устанавливает интсрумент для первого канала. Здесь мы используем еще одну команду Cx. Опять-таки вместо x используется номер канала, а второй байт представляет из себя номер инструмента, из списка, приведенного здесь.
GM 1 Sound Set
Для того, чтобы было удобнее пользоваться этим списком, лучше оформить его в виде перечисления. Я воспользовался расширением CustomButtons для Firefox и создал кнопку со следующим кодом
Код javascript Выделить
var headerRow = Array.prototype.filter.call( content.document.querySelectorAll("tr"), function(tr){return tr.textContent.search("Instrument Name") > -1;})[0];
var rows = headerRow.parentElement.querySelectorAll("tr");
var s = "public enum MusicalInstruments\r\n{\r\n";
for(var i = 3; i < rows.length; i++ )
{
  var row = rows[i];
  var tds = row.querySelectorAll("td");
  var name = tds[1].textContent.replace(/[\s\(\)\-\+]+/g, "_").replace(/_$/, "");
  var value = tds[0].textContent.substr(0, tds[0].textContent.length - 1);
  s += "    " + name + " = " + value + ",\r\n";
}
s += "}";
 
gClipboard.write(s);
Далее, открыл страницу и выполнил код кнопки. В результате в буфер обмена скопировался почти готовый код перечисления(там только запятую лишнюю в конце убрать надо).

Кликните здесь для просмотра всего текста
Код csharp Выделить
    public enum MusicalInstruments
    {
        Acoustic_Grand_Piano = 1,
        Bright_Acoustic_Piano = 2,
        Electric_Grand_Piano = 3,
        Honky_tonk_Piano = 4,
        Electric_Piano_1 = 5,
        Electric_Piano_2 = 6,
        Harpsichord = 7,
        Clavi = 8,
        Celesta = 9,
        Glockenspiel = 10,
        Music_Box = 11,
        Vibraphone = 12,
        Marimba = 13,
        Xylophone = 14,
        Tubular_Bells = 15,
        Dulcimer = 16,
        Drawbar_Organ = 17,
        Percussive_Organ = 18,
        Rock_Organ = 19,
        Church_Organ = 20,
        Reed_Organ = 21,
        Accordion = 22,
        Harmonica = 23,
        Tango_Accordion = 24,
        Acoustic_Guitar_nylon = 25,
        Acoustic_Guitar_steel = 26,
        Electric_Guitar_jazz = 27,
        Electric_Guitar_clean = 28,
        Electric_Guitar_muted = 29,
        Overdriven_Guitar = 30,
        Distortion_Guitar = 31,
        Guitar_harmonics = 32,
        Acoustic_Bass = 33,
        Electric_Bass_finger = 34,
        Electric_Bass_pick = 35,
        Fretless_Bass = 36,
        Slap_Bass_1 = 37,
        Slap_Bass_2 = 38,
        Synth_Bass_1 = 39,
        Synth_Bass_2 = 40,
        Violin = 41,
        Viola = 42,
        Cello = 43,
        Contrabass = 44,
        Tremolo_Strings = 45,
        Pizzicato_Strings = 46,
        Orchestral_Harp = 47,
        Timpani = 48,
        String_Ensemble_1 = 49,
        String_Ensemble_2 = 50,
        SynthStrings_1 = 51,
        SynthStrings_2 = 52,
        Choir_Aahs = 53,
        Voice_Oohs = 54,
        Synth_Voice = 55,
        Orchestra_Hit = 56,
        Trumpet = 57,
        Trombone = 58,
        Tuba = 59,
        Muted_Trumpet = 60,
        French_Horn = 61,
        Brass_Section = 62,
        SynthBrass_1 = 63,
        SynthBrass_2 = 64,
        Soprano_Sax = 65,
        Alto_Sax = 66,
        Tenor_Sax = 67,
        Baritone_Sax = 68,
        Oboe = 69,
        English_Horn = 70,
        Bassoon = 71,
        Clarinet = 72,
        Piccolo = 73,
        Flute = 74,
        Recorder = 75,
        Pan_Flute = 76,
        Blown_Bottle = 77,
        Shakuhachi = 78,
        Whistle = 79,
        Ocarina = 80,
        Lead_1_square = 81,
        Lead_2_sawtooth = 82,
        Lead_3_calliope = 83,
        Lead_4_chiff = 84,
        Lead_5_charang = 85,
        Lead_6_voice = 86,
        Lead_7_fifths = 87,
        Lead_8_bass_lead = 88,
        Pad_1_new_age = 89,
        Pad_2_warm = 90,
        Pad_3_polysynth = 91,
        Pad_4_choir = 92,
        Pad_5_bowed = 93,
        Pad_6_metallic = 94,
        Pad_7_halo = 95,
        Pad_8_sweep = 96,
        FX_1_rain = 97,
        FX_2_soundtrack = 98,
        FX_3_crystal = 99,
        FX_4_atmosphere = 100,
        FX_5_brightness = 101,
        FX_6_goblins = 102,
        FX_7_echoes = 103,
        FX_8_sci_fi = 104,
        Sitar = 105,
        Banjo = 106,
        Shamisen = 107,
        Koto = 108,
        Kalimba = 109,
        Bag_pipe = 110,
        Fiddle = 111,
        Shanai = 112,
        Tinkle_Bell = 113,
        Agogo = 114,
        Steel_Drums = 115,
        Woodblock = 116,
        Taiko_Drum = 117,
        Melodic_Tom = 118,
        Synth_Drum = 119,
        Reverse_Cymbal = 120,
        Guitar_Fret_Noise = 121,
        Breath_Noise = 122,
        Seashore = 123,
        Bird_Tweet = 124,
        Telephone_Ring = 125,
        Helicopter = 126,
        Applause = 127,
        Gunshot = 128
    }

Имея такое перечисление можно перегрузить метод SetInstrument вот таким методом.
Код csharp Выделить
       public void SetInstrument(MusicalInstruments id)
        {
            midiOutShortMsg(handle, (int)id << 8 | 0x000000C0);
        }
Пользоваться им намного удобнее.

Ну и наконец метод PlayNote я сделал похожим на метод Console.Beep, за исключением того, что первый аргумент - это не частота, а номер ноты. Таким образом примеры из предыдущей статьи сначала придется обработать, вычислив номер ноты по ее частоте. Вычисление это несложное, метод выглядит так
Код csharp Выделить
        int NoteNumberFromFriquency(double friquency)
        {
            return (int)Math.Round(69.0 + 12.0 * (Math.Log(friquency / 440.0) / Math.Log(2.0)));
        }
Теперь для того, чтобы преобразовать строку с числами для бипера, в которой у нас частоты чередуются с длительностями мы создадим следующий метод
Код csharp Выделить
        int[] BeepToMidi(string beep)
        {
            var result = new List<int>();
            var b = beep.Split(" \t\r\n".ToCharArray(), StringSplitOptions.RemoveEmptyEntries);
            for (int i = 0; i < b.Length; i += 2)
            {
                if (b[i] == "0")
                {
                    result.Add(-1);
                }
                else
                {
                    result.Add(NoteNumberFromFriquency(double.Parse(b[i])));
                }
                result.Add(int.Parse(b[i + 1]));
            }
            return result.ToArray();
        }
Он получает строку, а возвращает массив чисел, которые мы можем использовать. Вот так можно проиграть "Имперский марш" из статьи о бипере(инструмент, разумеется, можно выбрать и другой).
Код csharp Выделить
        void PlayImperialWithNotes()
        {
            var song = "440 500 440 500 440 500 349 350 523 150 440 500 349 350 523 150 440 1000 659 500 659 500 659 500 698 350 523 150 415 500 349 350 523 150 440 1000 880 500 440 350 440 150 880 500 830 250 784 250 740 125 698 125 740 250 0 250 455 250 622 500 587 250 554 250 523 125 466 125 523 250 0 250 349 125 415 500 349 375 440 125 523 500 440 375 523 125 659 1000 880 500 440 350 440 150 880 500 830 250 784 250 740 125 698 125 740 250 0 250 455 250 622 500 587 250 554 250 523 125 466 125 523 250 0 250 349 250 415 500 349 375 523 125 440 500 349 375 261 125 440 1000 0 100";
            var songInt = BeepToMidi(song);
            var player = new MelodyPlayer();
                player.Open();
                player.SetInstrument(44);
                for (int i = 0; i < songInt.Length; i += 2)
                {
                    if (songInt[i] == -1)
                    {
                        Thread.Sleep(songInt[i + 1]);
                    }
                    else
                    {
                        player.PlayNote(songInt[i], songInt[i + 1]);
                    }
                }
                player.Close();
        }
Формат, придуманный для "снятия" мелодий с нотного листа, тоже можно использовать, поскольку можно сначала пропустить его через парсер, а уже результат использовать в методах, описанных выше.

Комментариев нет :

Отправить комментарий