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

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

  1. Воспроизведение потока сообщений
  2. Аккомпанемент
  3. Объединение коллекций сообщений

Воспроизведение потока сообщений



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

Поскольку все сообщения имеют формат целого числа, равно как и продолжительность пауз между отправкой этих сообщений, конечный формат, который нужно будет воспроизвести, можно представить в виде коллекции целых чисел. Каждое число в этой коллекции будет отдельным сообщением, а для пауз можно использовать опять-таки нуль, который будет означать, что следующее за ним число - продолжительность паузы. То есть при воспроизведении мы будем отправлять все сообщения до тех пор пока не наткнемся на нуль, после чего остановим процесс на время указанное числом, следующим за нулем и далее со следующими сообщениями будем делать то же самое пока коллекция не закончится. Для воспроизведения такого потока сообщений создадим в нашем классе MelodyPlayer свойство
Код csharp Выделить
public int[] MsgList { get; set; }
И метод
Код csharp Выделить
        public void Play()
        {
            for (int i = 0; i < MsgList.Length; i++)
            {
                if (MsgList[i] == 0)
                {
                    i++;
                    Thread.Sleep(MsgList[i]);
                }
                else
                {
                    midiOutShortMsg(handle, MsgList[i]);
 
                };
            }
        }
Можно обойтись без свойства и передавать массив сообщений как аргумент метода (дело вкуса).

Далее эксперименты будем проводить с известной мелодией "Полет кондора", ноты которой приведены во вложении. Используя формат описанный раннее, из этих нот мы получаем следующий код
Код:
0 3000 
1e4 |
1a8 1g+8 1a8 1b8 2c8 1b8 2c8 2d8 | 2e2,4 2g4 |   2e2,4 2a8 2g8 | 2e2,8 2e8 2d8 2c8 | 2d16 2c16 1a2 x8 2c4 | 1a2,4,8 1e8 |
1a8 1g+8 1a8 1b8 2c8 1b8 2c8 2d8 | 2e2,4 2g4 |   2e2,4 2a8 2g8 | 2e2,8 2e8 2d8 2c8 | 2d16 2c16 1a2 x8 2c4 | 1a2,4,8 2e8 |

2a4,8 2g+8 2a8,16 2g16 2a8,16 2b16 | 3c2 x8 2a8 3c8 2a8 | 2g2 x4 2a8 2g8 | 2e2,4 2e4 |
2a4,8 2g+8 2a8,16 2g16 2a8,16 2b16 | 3c2 x8 2a8 3c8 2a8 | 2g2 x4 2a8 2g8 | 2e2 x8 2e8 2d8 2c8 | 2d16 2c16 1a2 x8 2c4 | 1a2,4,8 2e8

2a4,8 2g+8 2a8,16 2g16 2a8,16 2b16 | 3c2 x8 2a8 3c8 2a8 | 2g2 x4 2a8 2g8 | 2e2,4 2e4 |
2a4,8 2g+8 2a8,16 2g16 2a8,16 2b16 | 3c2 x8 2a8 3c8 2a8 | 2g2 x4 2a8 2g8 | 2e2 x8 2e8 2d8 2c8 | 2d16 2c16 1a2 x8 2c4 | 1a2,4
Для обработки этого кода используем парсер, также описанный ранее. В результате обработки парсером, мы получим код для бипера, а нам нужна коллекция сообщений. Для преобразования кодов бипера в коллекцию сообщения добавим вот такой метод.
Код csharp Выделить
        public static int[] BeepToMidiMessages(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)
            {
                var duration = int.Parse(b[i + 1]);
                if (b[i] == "0")
                {
                    result.Add(0);
                    result.Add(duration);
                }
                else
                {
                    var note = NoteNumberFromFriquency(double.Parse(b[i]));
                    var msg = 64 << 16 | note << 8 | 0x90;
                    result.Add(msg);
                    result.Add(0);
                    result.Add(duration);
                    result.Add(note << 8 | 0x80);
 
                }
            }
            return result.ToArray();
        }
Теперь добавим код мелодии в ресурсы под именем ElCondorPasa и для воспроизведения нам потребуется следующий код
Код csharp Выделить
        public static void PlayElCondorPasaWithMessages()
        {
            var player = new MelodyPlayer();
            var parser = new MusicFileParser();
            player.MsgList = FormatConverter.BeepToMidiMessages(parser.Parse(Properties.Resources.ElCondorPasa));
            player.Open();
            player.Play();
            player.Close();
        }
Здесь FormatConverter - это класс, в котором я разместил статический метод BeepToMidiMessages, приведенный выше.

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

Теперь нам, например, не нужно вызывать SetInstrument, поскольку мы можем встроить в коллекцию соответствующее сообщение. В строке, где мы инициируем свойство MsgList можно написать так
Код csharp Выделить
            player.MsgList = new int[] { 78 << 8 | 0xC0 }.Concat(  FormatConverter.BeepToMidiMessages(parser.Parse(Properties.Resources.ElCondorPasa))).ToArray();
И мелодия будет исполнена на сякухати, получается очень красиво.

Аккомпанемент



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

Мелодия "Полета кондора" достаточно проста, да и гармония тоже. Обыграть ее можно тремя аккордами: ля-минор, до-мажор и фа-мажор. Для того чтобы упростить задачу создания аккомпанемента создадим метод который будет создавать коллекцию сообщений, необходимых для обыгрывания одного аккорда.
Код csharp Выделить
        int[] GetChord(string chord)
        {
            var velocity = 64;
            var full = 3000;
            var prima = CalculateNote3(chord[0]) + 12;
            if (chord.Length > 1)
            {
                switch (chord[1])
                {
                    case '+': prima++; break;
                    case '-': prima--; break;
                    default:
                        break;
                }
            }
            var maj = char.IsUpper(chord[0]);
            var b2 = prima - 5;
            var ter = prima + (maj ? 4 : 3);
            var qu = prima + 7;
            var oct = prima + 12;
            var result = new List<int>();
            result.Add(velocity << 16 | prima << 8 | 0x91);
            result.Add(0);
            result.Add(full / 8);
            result.Add(velocity << 16 | ter << 8 | 0x91);
            result.Add(velocity << 16 | qu << 8 | 0x91);
            result.Add(velocity << 16 | oct << 8 | 0x91);
            result.Add(0);
            result.Add(full / 16);
            result.Add(ter << 8 | 0x81);
            result.Add(qu << 8 | 0x81);
            result.Add(oct << 8 | 0x81);
            result.Add(velocity << 16 | ter << 8 | 0x91);
            result.Add(velocity << 16 | qu << 8 | 0x91);
            result.Add(velocity << 16 | oct << 8 | 0x91);
            result.Add(0);
            result.Add(full / 16);
            result.Add(ter << 8 | 0x81);
            result.Add(qu << 8 | 0x81);
            result.Add(oct << 8 | 0x81);
            result.Add(prima << 8 | 0x81);
 
            result.Add(velocity << 16 | b2 << 8 | 0x91);
            result.Add(0);
            result.Add(full / 8);
            result.Add(velocity << 16 | ter << 8 | 0x91);
            result.Add(velocity << 16 | qu << 8 | 0x91);
            result.Add(velocity << 16 | oct << 8 | 0x91);
            result.Add(0);
            result.Add(full / 8);
            result.Add(ter << 8 | 0x81);
            result.Add(qu << 8 | 0x81);
            result.Add(oct << 8 | 0x81);
            result.Add(b2 << 8 | 0x81);
            return result.ToArray();
        }
 
        int CalculateNote3(char n)
        {
            switch (char.ToLower(n))
            {
                case 'a': return 45;
                case 'b': return 47;
                case 'c': return 48;
                case 'd': return 50;
                case 'e': return 52;
                case 'f': return 53;
                case 'g': return 55;
                default:
                    throw new ArgumentException();
            }
        }
Рисунок, который он выдает - достаточно прост и занимает полтакта. Чтобы послушать, что этот метод делает, можно запустить такой код
Код csharp Выделить
            var player = new MelodyPlayer();
            var list = "a a d d B- E a a".Split(" \r\n\t".ToCharArray(), StringSplitOptions.RemoveEmptyEntries).SelectMany(i => GetChord(i));
            player.MsgList = new int[] { 25 << 8 | 0xC1 }.Concat(list).ToArray();
            player.Open();
            player.Play();
            player.Close();
Здесь a a d d B- E a a - последовательность аккордов. Малая буква - аккорд минорный, Большая - мажорный, плюс после буквы - диез, минус - бемоль.

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

Ну и собственно гармония для нашей мелодии будет выглядеть так
Код:
a a C C C C C C a a a a 
a a C C C C C C a a a a
F F F F C C C C 
F F F F C C C C a a a a
А весь код, который ее проигрывает - так
Код csharp Выделить
            var player = new MelodyPlayer();
            var list = @"
a a C C C C C C a a a a 
a a C C C C C C a a a a
F F F F C C C C 
F F F F C C C C a a a a".Split(" \r\n\t".ToCharArray(), StringSplitOptions.RemoveEmptyEntries).SelectMany(i => GetChord(i));
            player.MsgList = new int[] { 25 << 8 | 0xC1, 0 , 3000 / 4 }.Concat(list).ToArray();
            player.Open();
            player.Play();
            player.Close();
Здесь еще в коллекцию сообщений добавлена установка инструмента №25 - это акустическая гитара с нейлоновыми струнами и пауза на одну четверть вначале, поскольку мелодия начинается с затакта длительностью в одну четверть, а аккорды будут начинать звучать в начале такта.


Объединение коллекций сообщений



На данный момент мы можем проиграть мелодию и аккорды к ней. Одна беда - сделать мы можем это раздельно. Теперь нам нужно объединить две коллекции сообщений в одну.

Для того, чтобы упростить алгоритм объединения, создадим вспомогательный класс
Код csharp Выделить
    class MessagesPackage
    {
        public int TimeLine { get; set; }
        public List<int> MessagesList { get; set; } = new List<int>();
    }
В экземпляр этого класса будут размещаться сообщения, которые надо отправить одновременно (свойство MessageesList), а свойство Timeline будет хранить количество миллисекунд, прошедших с начала воспроизведения. То есть благодаря такому объекту мы будем точно знать в какой момент времени пакет сообщений должен быть отправлен.

Далее, нам надо конвертировать коллекцию сообщений с которой мы работали ранее в коллекцию экземпляров MessagePackage.
Код csharp Выделить
        List<MessagesPackage> GetPackagesList(int[] msgs)
        {
            int timeline = 0;
            int counter = 0;
            List<MessagesPackage> result = new List<midi.MessagesPackage>();
 
            while (true)
            {
                if (msgs.Length <= counter) break;
                if (msgs[counter] == 0)
                {
                    counter++;
                    timeline += msgs[counter];
                    result.Add(new midi.MessagesPackage() { TimeLine = timeline });
                }
                else
                {
                    if (result.Count == 0) result.Add(new MessagesPackage() { TimeLine = 0 });
                    result.Last().MessagesList.Add(msgs[counter]);
                }
                counter++;
            }
 
            return result;
        }
Алгоритм довольно прост: коллекцию сообщений пакета образуют все сообщения между двумя паузами, а состояние таймлайна определяется как сумма продолжительностей всей пауз, предшествующих данному блоку сообщений.

Теперь для слияния нескольких коллекций сообщений, их все надо преобразовать в коллекции пакетов, объединить, отсортировать по значению свойства TimeLine и объединить сообщения, вставляя на границах пакетов паузы. Продолжительность пауз определяется разностью между значением состояния таймланов текущего и предыдущего пакетов(если она равна нулю - пауза не вставляется).
Код csharp Выделить
        public int[] MergeMessagesLists(params int[][] lists)
        {
            var mplist = lists.SelectMany(GetPackagesList).OrderBy(mp => mp.TimeLine).ToArray();
            for (int i = 0; i < mplist.Count(); i++)
            {
                if (i > 0 && mplist[i].TimeLine == mplist[i - 1].TimeLine) continue;
                mplist[i].MessagesList.InsertRange(0, new int[] { 0, i == 0 ? mplist[i].TimeLine : mplist[i].TimeLine - mplist[i - 1].TimeLine });
            }
            return mplist.SelectMany(mp => mp.MessagesList).ToArray();
        }
Ну и теперь, наконец, мы можем объединить мелодию и аккорды и уже послушать, что получилось.

Код csharp Выделить
            var parser = new MusicFileParser();
 
            var chords = @"
a a C C C C C C a a a a 
a a C C C C C C a a a a
F F F F C C C C 
F F F F C C C C a a a a
F F F F C C C C 
F F F F C C C C a a a a
".Split(" \r\n\t".ToCharArray(), StringSplitOptions.RemoveEmptyEntries).SelectMany(GetChord).ToArray();
            chords = new int[] { 25 << 8 | 0xC1, 0, 3000 / 4 }.Concat(chords).ToArray();
            var melody = new int[] { 78 << 8 | 0xC0 }.Concat(FormatConverter.BeepToMidiMessages(parser.Parse(Properties.Resources.ElCondorPasa))).ToArray();
            var player = new MelodyPlayer();
            player.MsgList = MergeMessagesLists(chords, melody);
            player.Open();
            player.Play();
            player.Close();
Во вложении проект, содержащий коды из статей с возможностью проиграть мелодии.

Третья часть статьи - в планах. ))
Миниатюры
Вложения

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

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