Воспроизведение потока сообщений
Описанный выше метод воспроизведения мелодии - на самом деле не очень хорош. Он довольно просто реализуется и хорош тем, что в результате мы получаем методику очень похожую на исполнение мелодий на бипере, когда для каждой ноты вызывается метод, которому передается информация о ноте и ее длительности. Это было хорошо для целей демонстрации, но если мы захотим немного расширить наши возможности, то метод придется пересмотреть. Я уже написал, что для воспроизведения полноценной музыки наша программа должна в определенные моменты времени посылать определенные сообщения, причем сообщений может быть достаточно много и они совсем не обязательно должны требовать нажатия или отпускания клавиш или работать на одном лишь канале.
Поскольку все сообщения имеют формат целого числа, равно как и продолжительность пауз между отправкой этих сообщений, конечный формат, который нужно будет воспроизвести, можно представить в виде коллекции целых чисел. Каждое число в этой коллекции будет отдельным сообщением, а для пауз можно использовать опять-таки нуль, который будет означать, что следующее за ним число - продолжительность паузы. То есть при воспроизведении мы будем отправлять все сообщения до тех пор пока не наткнемся на нуль, после чего остановим процесс на время указанное числом, следующим за нулем и далее со следующими сообщениями будем делать то же самое пока коллекция не закончится. Для воспроизведения такого потока сообщений создадим в нашем классе 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();
}
Код 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();
}
Здесь все записывается в первый канал, но это легко исправить, а для демонстрации лучше не усложнять код подробностями, на которых пока не стоит сосредотачиваться.
Теперь нам, например, не нужно вызывать 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();
Объединение коллекций сообщений
На данный момент мы можем проиграть мелодию и аккорды к ней. Одна беда - сделать мы можем это раздельно. Теперь нам нужно объединить две коллекции сообщений в одну.
Для того, чтобы упростить алгоритм объединения, создадим вспомогательный класс
Код csharp | Выделить |
class MessagesPackage
{
public int TimeLine { get; set; }
public List<int> MessagesList { get; set; } = new List<int>();
}
Далее, нам надо конвертировать коллекцию сообщений с которой мы работали ранее в коллекцию экземпляров 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();
Третья часть статьи - в планах. ))
Комментариев нет :
Отправить комментарий