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

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

  1. Шаблоны аккордов
  2. Другой подход к шаблонизации аккордов
  3. Воспроизведение с использованием таймера

Шаблоны аккордов



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

Вместо нот будем использовать числа, которые показывают хроматический интервал по отношению к условному основному тону. Условному потому что использовать его можно будет и для обращений аккордов и в этом случае в качестве основного тона будет задаваться не прима аккорда. То есть сам основной тон будет иметь значение 0, нота на квинту выше - 7, а на кварту ниже - -5 и так далее. После числа, обозначающего ноту, будет идти точка, за которой последует длительность, в формате описанном ранее ( 4 означает четвертинную ноту, 4,8 - четверть с точкой и т. д.). Для пауз но та будет отсутствовать, то есть пауза начинается с точки. Все это будет писаться через пробелы. Но, когда мы создавали формат для записи мелодий, там все было немного проще: одна нота заканчивает звучать - начинает звучать следующая. Здесь же ноты могут звучать одновременно или, например, "внахлест"(то есть одна еще не закончила звучать, а следующая уже начинает). Здесь решение в том, что каждый голос будет записываться на отдельной строке. Таким образом аккорды из второй части статьи будут иметь следующие шаблоны:
Для мажорного трезвучия
Код:
0.4               -5.4
.8 4.16 4.16      .8 4.8
.8 7.16 7.16      .8 7.8
.8 12.16 12.16   .8 12.8
Для минорного
Код:
0.4               -5.4
.8 3.16 3.16      .8 3.8
.8 7.16 7.16      .8 7.8
.8 12.16 12.16   .8 12.8
Создания коллекции сообщений на основе такого шаблона создадим следующий метод
Код csharp Выделить
        public static int[] CreateChordFromTemplate(string template, int prima, int fullNoteDuration, int velocity, int channel)
        {
            var re = new Regex(@"(?<position>(\-?\d+)?)\.(?<duration>\d+(,\d+)*)", RegexOptions.Compiled);
 
            Func<string, int[]> melodyFromLine = line =>
            {
                var matches = re.Matches(line);
                var res = new List<int>();
                foreach (Match m in matches)
                {
                    var position = m.Groups["position"].Value;
                    var duration = m.Groups["duration"].Value.Split(',').Select(d => fullNoteDuration / int.Parse(d)).Sum();
                    if(position == "")
                    {
                        res.Add(0);
                        res.Add(duration);
                    }
                    else
                    {
                        var note = prima + int.Parse(position);
                        res.Add(velocity << 16 | note << 8 | 0x90 | channel);
                        res.Add(0);
                        res.Add(duration);
                        res.Add(note << 8 | 0x80 | channel);
                    }
                }
                return res.ToArray();
            };
 
            var lines = template.Split("\r\n".ToCharArray(), StringSplitOptions.RemoveEmptyEntries).Select(melodyFromLine).ToArray();
            return MessageProcessor.MergeMessagesLists(lines);
        }
Теперь нам нужен способ записать последовательность аккордов, использующих эти шаблоны. Здесь все просто: запись аккорда будет состоять из номера октавы(как он определен для MIDI-инструментов (от 0 до 9). После чего символ ноты в нижнем регистре, далее плюс или минус, в случае, если нужен диез или бемоль, далее точка и имя шаблона. Шаблоны в тестовом примере я разместил в ресурсы под именами ChordTemplate_TemplateName, где вместо TemplateName идет реальное имя шаблона, которое будет использоваться в записи аккордов. У меня имена простые: 1, 2, 3, 4. Но, если их много, то проще будет давать осмысленные имена. Имена могут состоять из букв, цифр и знаков подчеркивания.
Код csharp Выделить
        public static int[] ParseChords(string chords, int fullDuration, int velocity, int channel)
        {
            var re = new Regex(@"(?<octave>\d)(?<note>[a-g])(?<alter>[\+\-]?)\.(?<template>\w+)", RegexOptions.Compiled);
            Func<Match, int[]> getmsgs = match =>
            {
                var note = CalculateNote0(match.Groups["note"].Value[0]) + int.Parse(match.Groups["octave"].Value) * 12 + (match.Groups["alter"].Value == "+" ? 1 : -1 );
                var template = Properties.Resources.ResourceManager.GetString("ChordTemplate_" + match.Groups["template"].Value);
                return CreateChordFromTemplate(template, note, fullDuration, velocity, channel);
            };
            return (from Match m in re.Matches(chords) select m).SelectMany(getmsgs).ToArray();
        }
 
        static int CalculateNote0(char n)
        {
            switch (char.ToLower(n))
            {
                case 'a': return 9;
                case 'b': return 11;
                case 'c': return 0;
                case 'd': return 2;
                case 'e': return 4;
                case 'f': return 5;
                case 'g': return 7;
                default:
                    throw new ArgumentException();
            }
        }

В примере из вложения используется следующий код
Код csharp Выделить
            var parser = new MusicFileParser();
 
            var chords = ChordMaster.ParseChords(@"5a.2 5a.2 5d.2 5d.2 5b-.1 5e.1 5a.2 5a.2", 3000, 127, 1);
            chords = new int[] { 25 << 8 | 0xC1, 0, 3000 / 4 }.Concat(chords).ToArray();
            var player = new MelodyPlayer();
            player.MsgList = chords;
            player.Open();
            player.Play();
            player.Close();
и
Код csharp Выделить
            var parser = new MusicFileParser();
 
            var chords = ChordMaster.ParseChords(@"4a.4 4a.4 4d.4 4d.4 4b-.3 4e.3 4a.4 4a.4", 3000, 127, 1);
            chords = new int[] { 25 << 8 | 0xC1, 0, 3000 / 4 }.Concat(chords).ToArray();
            var player = new MelodyPlayer();
            player.MsgList = chords;
            player.Open();
            player.Play();
            player.Close();
Шаблоны 1 и 2 приведены выше, а 3 и 4 есть в ресурсах проекта.


Другой подход к шаблонизации аккордов



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

В данном случае сделать это - совсем несложно. Просто в шаблонах мы обозначали ноты числами, соответствующими количеству хроматических полутонов на которые нота отстоит от основного тона, а в метод обработки передавали основной тон. Вместо этого можно в метод обработки передавать коллекцию нот аккорда, при этом числа в шаблонах будут обозначать индекс ноты в этой коллекции. Например возьмем такой шаблон
Код:
0.4             1.4
.8 2.16 2.16      .8 2.8
.8 3.16 3.16      .8 3.8
.8 4.16 4.16    .8 4.8
Если мы передадим ему ноты мажорного аккорда, то он будет работать как первый шаблон из предыдущего параграфа, а если минорного - как второй. Но теперь у нас появляется возможность передавать ему множество других аккордов.

Естественно, для обработки таких шаблонов нам нужно добавить перегрузку метода CreateChordFromTemplate, которая вместо примы аккорда будет принимать массив, содержащий все звуки, упомянутые в шаблоне. Собственно изменения в коде метода тоже будут минимальными, всего лишь придется изменить строку, вычисляющую номер ноты.
Код csharp Выделить
        public static int[] CreateChordFromTemplate(string template, int[] notes, int fullNoteDuration, int velocity, int channel)
        {
            var re = new Regex(@"(?<position>(\-?\d+)?)\.(?<duration>\d+(,\d+)*)", RegexOptions.Compiled);
 
            Func<string, int[]> melodyFromLine = line =>
            {
                var matches = re.Matches(line);
                var res = new List<int>();
                foreach (Match m in matches)
                {
                    var position = m.Groups["position"].Value;
                    var duration = m.Groups["duration"].Value.Split(',').Select(d => fullNoteDuration / int.Parse(d)).Sum();
                    if (position == "")
                    {
                        res.Add(0);
                        res.Add(duration);
                    }
                    else
                    {
                        var note = notes[int.Parse(position)];
                        res.Add(velocity << 16 | note << 8 | 0x90 | channel);
                        res.Add(0);
                        res.Add(duration);
                        res.Add(note << 8 | 0x80 | channel);
                    }
                }
                return res.ToArray();
            };
 
            var lines = template.Split("\r\n".ToCharArray(), StringSplitOptions.RemoveEmptyEntries).Select(melodyFromLine).ToArray();
            return MessageProcessor.MergeMessagesLists(lines);
        }
Теперь можно ввести обозначения для аккордов, похожие на стандартные обозначения, но такие, чтобы их можно было легко записать как обычный текст и легко парсить. Ну например:
  1. Большая буква - мажорный аккорд, малая - минорный (избавляемся от необходимости писать и парсить букву m)
  2. Все обозначение ступеней, присутствующие в стандартной записи, пишем в формате [+-]?\d\d?(,[+-]?\d\d?)*
  3. При этом заменяем обозначения типа maj dim sus2 sus4 и т. д. обозначениями из п. 2. То есть, вместо Csus2 будем писать c-3, вместо Csus4 - С+3, вместо Cdim7 - c-5,-7, вместо Cmaj7 - C+7 и т. д.

В двух словах формат выглядит так:
Буква аккорда (большая или малая, в зависимости от того минор это или мажор), далее идут через запятую все альтерированные ступени. Ступень с самым большим интервалом подразумевает, что все предшествующие терцовые (нечетные) ступени тоже присутствуют. Секста используется как добавочный звук, поэтому она присутствует только там, где указана. Для парсинга таких аккордов можно использовать такой код:
Код csharp Выделить
        public static int[] ParseChord(string chord)
        {
            var re = new Regex(@"(?<note>[a-gA-G])(?<steps>[+-]?\d\d?(,[+-]?\d\d?)*)?", RegexOptions.Compiled);
            var match = re.Match(chord);
            var note = match.Groups["note"].Value;
            var steps = match.Groups["steps"].Value.Split(',');
            var intSteps = steps.Where(s => s != "").Select(x => Math.Abs(int.Parse(x)));
            var result = new List<int>() { CalculateNote0(note[0]) };
            Action<int, int> addStep = (step, norm) =>
            {
                var ss = steps.FirstOrDefault(s => s == "+" + step.ToString() || s == "-" + step.ToString() || s == step.ToString());
                if (ss == null)
                {
                    result.Add(result[0] + norm);
                }
                else
                {
                    switch (ss.Substring(0, 1))
                    {
                        case "+": result.Add(result[0] + norm + 1); break;
                        case "-": result.Add(result[0] + norm - 1); break;
                        default: result.Add(result[0] + norm); break;
                    }
                }
            };
 
            addStep(3, char.IsLower(note[0]) ? 3 : 4);
            addStep(5, 7);
            var maxStep = intSteps.Count() == 0 ? 5 : intSteps.Max();
            var restSteps = "6:9 7:11 9:14 11:17 13:19".Split(' ').ToDictionary(s => int.Parse(s.Split(':')[0]), s => int.Parse(s.Split(':')[1]));
            foreach (int rs in restSteps.Keys)
            {
                if (rs > maxStep) break;
                if (intSteps.Contains(rs))
                    addStep(rs, restSteps[rs]);
            }
            return result.ToArray();
        }
Он возвратит массив чисел, соответсвующих нотам аккорда на нулевой октаве.
В проекте во вложении есть специальная форма, которая позволяет протестировать аккорды. Там в одном текстовом поле можно ввести код аккорда, а во втором - код шаблона, для того, чтобы аккорд можно было обыграть. Естественно, для реальной работы с аккордами лучше было бы создать отдельный класс, в который можно было бы заложить основные принципы работы аккорда. Шаблон обычно рассчитан на определенное количество нот, в то время как у разных аккордов их количество может отличаться. Кроме того, там могут быть предусмотрены повторения некоторых нот на разных октавах.

Таким образом, того, что здесь представленно - недостаточно, но по крайней мере уже есть от чего оттолкнуться. Далее углубляться в эту тему не буду.

Воспроизведение с использованием таймера



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

Для экспериментов с таймером мы сначала выполним некоторые преобразования существующего кода. Нам понадобится новый класс для воспроизведения мелодий, но частично он будет содержать прежний код. Поэтому мы вынесем часть кода из класса MelodyPlayer в новый класс PlayerBase, заменим модификаторы private на protected и унаследуем MelodyPlayer от этого нового класса.
Кликните здесь для просмотра всего текста
Код csharp Выделить
using System.Runtime.InteropServices;
 
namespace midi
{
    public abstract class PlayerBase
    {
        [DllImport("winmm.dll")]
        protected 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);
 
        protected delegate void MidiCallBack(int handle, int msg,
   int instance, int param1, int param2);
 
 
        protected int handle = 0;
 
        public int[] MsgList { get; set; }
 
        public void Open()
        {
            midiOutOpen(ref handle, 0, null, 0, 0);
        }
 
        public void Close()
        {
            midiOutClose(handle);
        }
 
        public abstract void Play();
 
    }
}
Код csharp Выделить
using System.Runtime.InteropServices;
using System.Threading;
 
namespace midi
{
using System.Runtime.InteropServices;
using System.Threading;
 
namespace midi
{
    class MelodyPlayer : PlayerBase
    {
        public void SetInstrument(int id)
        {
            midiOutShortMsg(handle, id << 8 | 0x000000C0);
        }
 
        public void PlayNote(int note, int duration)
        {
            midiOutShortMsg(handle, 127 << 16 | note << 8 | 0x00000090);
            Thread.Sleep(duration);
            midiOutShortMsg(handle, 127 << 16 | note << 8 | 0x00000080);
        }
 
        public void SetInstrument(MusicalInstruments id)
        {
            midiOutShortMsg(handle, (int)id << 8 | 0x000000C0);
        }
 
        public override void Play()
        {
            for (int i = 0; i < MsgList.Length; i++)
            {
                if (MsgList[i] == 0)
                {
                    i++;
                    Thread.Sleep(MsgList[i]);
                }
                else
                {
                    midiOutShortMsg(handle, MsgList[i]);
 
                };
            }
        }
    }
}


Создадим новый класс TimerPlayer, который также будет наследовать PlayerBase. Теперь нам надо решить одну небольшую проблему. Когда будет происходить срабатывание таймера в методе-коллбеке нам надо будет решить, что именно надо отправить из имеющихся сообщений. Ну допустим создадим мы счетчик, который будет указывать на последнее отправленное сообщение, но в таком случае, дойдем мы до паузы, а она может иметь различную длительность. В этом случае придется пропустить несколько тиков таймера и их тоже надо будет как-то учитывать. Сделать это возможно, но по-моему идея не очень хорошая. Поэтому я решил сделать иначе. Ранее для объединения нескольких наборов сообщений мы создали вспомогательный класс MessagesPackage. Этот класс хорош тем, что в нем есть свойство из которого можно точно узнать в какой момент времени нужно отправлять содержащиеся в нем сообщения. Таким образом если мы будем вести учет времени - то есть прост считать, сколько прошло от начала воспроизведения - то, сравнив текущее значение таймлайна со значением данного свойства нашего пакета мы будем точно знать, следует ли отправить сообщения пакета в данный момент. Кроме того, в дальнейшем это поможет нам воспроизводить произведение с любого момента таймлайна и организовывать паузы. Ну и опять-таки код преобразования массива сообщений в список пакетов мы уже написали ранее. На данный момент код нашего плеера будет следующим.
Кликните здесь для просмотра всего текста
Код csharp Выделить
using System.Collections.Generic;
using System.Threading;
namespace midi
{
    class TimerPlayer : PlayerBase
    {
 
        private Queue<MessagesPackage> pkgqueue;
        private Timer timer;
 
 
        public int TimeLine { get; set; }
        public int TimerInterval { get; set; } = 50;
        private void TimerTick(object state)
        {
            if (pkgqueue.Peek() == null)
            {
                timer.Dispose();
                TimeLine = 0;
                Close();
                return;
            }
            while (pkgqueue.Peek().TimeLine <= TimeLine)
            {
                pkgqueue.Dequeue().MessagesList.ForEach(msg => midiOutShortMsg(handle, msg));
            }
            TimeLine += TimerInterval;
        }
 
 
 
        private void CreatePackageQueue()
        {
            var msglist = MessageProcessor.GetPackagesList(this.MsgList);
            pkgqueue = new Queue<midi.MessagesPackage>(msglist);
        }
 
 
        public void GoOn()
        {
 
        }
 
        public override void Play()
        {
            TimeLine = 0;
            CreatePackageQueue();
            timer = new Timer(TimerTick, null, 0, TimerInterval);
        }
    }
}
А запустить это можно будет так
Код csharp Выделить
            var player = new TimerPlayer();
            var parser = new MusicFileParser();
            player.MsgList = new int[] { 78 << 8 | 0xC0 }.Concat(FormatConverter.BeepToMidiMessages(parser.Parse(Properties.Resources.ElCondorPasa))).ToArray();
            player.Open();
            player.Play();
В проекте из вложения можно посмотреть этот код в работе.
Вложения

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

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