/// с числом вероятностей. Вероятность находится в отрезке [0, 100].
/// Сумма вероятностей должна быть равна 100</param>
public MyRandom(int[] values, int[] probabilities)
{
section = new int[100];
random = new Random();
if (values. Length!= probabilities. Length)
{
MessageBox. Show("Ошибка программы.\nЧисло объектов и вероятностей не совпадает!");
return;
}
int sum = 0;
for (int i = 0; i < probabilities. Length; i++)
{
sum += probabilities[i];
}
if (sum!= 100)
{
MessageBox. Show("Ошибка программы.\nСумма вероятностей не равна 100!");
}
int j = 0, k = 0;
for (int i = 0; i < values. Length; i++)
{
while (j < probabilities[i] && k < 100)
{
section[k] = values[i];
j++;
k++;
}
j = 0;
}
}
public int Next()
{
return section[random. Next(0, 100)];
}
…
}
Остался нерешенным вопрос о точном распределении сдвигов. Было решено сделать для каждой гаммы (для каждого стиля) своё распределение. Вот пример кода:
if (scaleName == ScaleName. Flamenco && values. Length == 8)
{
probabilities = new int[] { 2, 75, 9, 3, 3, 3, 3, 2 };
}
На первом месте с вероятностью 2 находится сдвиг на 0 интервалов. Т. е. это повтор ноты. Или так называемый интервал прима. Для фламенко мы сделали более вероятным сдвиг на одну ступень для того, чтобы лучше услышать характерное звучание гаммы фламенко.
2.5. Кодирование и генерация ритма
Перейдем ко второй самой важной части в генерации мелодии. В предыдущей версии программы мелодия не имела ритма. Точнее, мелодия имела самый примитивный ритм, который можно воспринимать как его отсутствие. Она звучала монотонно, через равные промежутки времени. Это являлось существенным недостатком. В данной версии программы этот недостаток был устранен. После появления ритма мелодии зазвучали совершенно по-новому. На самом деле, ритм является самой настоящей «душой» мелодии. Часто именно ритм в большей степени определяет стиль, нежели гамма.
Возникает множество вопросов. Что такое ритм? Как его представлять в коде? Как его генерировать? Ответ на вопрос о том, что такое ритм, был дан в первой главе. Сейчас мы поговорим насчет машинного представления ритма. После этого уже нетрудно будет разработать алгоритм генерации.
Мы уже поняли, что ритм – это совокупность нот и пауз. Первая мысль – хранить эти длительности нот и пауз в виде промежутков времени. Но так мы будем хранить еще и темп, а мелодия и темп должны быть полностью независимы. В таком случае, лучше всего хранить не абсолютное значение промежутка времени, а относительное. Таким образом время всей мелодии делится на равные доли, и каждая доля имеет своё соответствующее значение в двоичном векторе. 1 – играется нота, 0 – пауза. Получается мы храним двоичные вектора. При таком проектировании достигается гибкость, так как темп мелодии никак не будет зависеть от ритма. Получается, что мелодия не зависит ни от тоники, ни от темпа, что и соответствует действительности. В самом деле, мелодия ни коим образом не зависит от выбранной тоники и от темпа. При изменении этих параметров, мелодия должна оставаться прежней.
После таких умозаключений не составит труда разработать алгоритм генерации ритма. Нам нужно сгенерировать случайный двоичный вектор. Каких-либо дополнительных рекомендаций на этот счет не будет. Однако стоит подумать насчет количества единиц и нулей в нашем векторе. В определенный момент времени у нас должны быть известны эти значения. Таким образом у нас есть вектор заданной длины, заполненный нулями. У нас есть определенное количество единиц, которые мы должны случайным образом распределить по этому вектору. На каждом шаге цикла мы должны случайным образом приписать единицу в случайное место массива. Но на каком-то шаге мы можем попытаться приписать единицу на уже занятое место массива, тогда нам нужно заново вызывать метод Random. Next(), и так, пока мы не найдем свободное место в массиве. Но есть более изящное решение. Все свободные места массива мы добавим в множество. И после того, как на шаге цикла мы приписали единицу на свободное место, мы удаляем это место из множества. Теперь нам остается выбирать свободное место из уже меньшего числа объектов. Вот пример программы:
Файл Rhythm. cs
/// <summary>
/// Метод возвращает массив, описывающий ритм
/// </summary>
/// <param name="segmentsCount">Количество долей в такте (количество нот и пауз)</param>
/// <param name="notesCount">Количество звучащих нот</param>
/// <returns></returns>
public static int[] GetRhythm(int segmentsCount, int notesCount)
{
HashSet<int> set = new HashSet<int>();
for (int i = 0; i < segmentsCount; i++)
{
set. Add(i);
}
int[] rhythm = new int[segmentsCount];
Random rand = new Random();
for (int i = 0; i < notesCount; i++)
{
int pos = rand. Next(0, segmentsCount);
int position = set. ElementAt(pos);
rhythm[position] = 1;
set. Remove(position);
segmentsCount--;
}
return rhythm;
}
Вот так лаконично и изящно мы решили очень важную проблему генерации мелодии. Решение не является универсальным. Алгоритм никак не учитывает особенности стиля, хотя, возможно, его будет несложно расширить и на другие случаи. Тем не менее, алгоритм дает достаточно хорошие результаты.
2.6. Композиция. Воспроизведение
Теперь у нас все готово для того, чтобы получилась мелодия, и чтобы её можно было воспроизвести. Как мы говорили, мелодия = ноты + ритм. Осталось подумать, как скомпоновать вместе ритм и ноты, для того, чтобы получилось корректно воспроизвести мелодию. Для этих целей нам понадобится класс Melody, который ничем нас не удивит:
public class Melody
{
public string Name { set; get; }
public int[] Notes;
public int[] Rhythm;
public ScaleName ScaleName;
public static int Number = 1;
public Melody(string name, int[] notes, int[] rhythm, ScaleName scaleName)
{
int sum = m();
if (sum > notes. Length)
{
int[] newNotes = new int[sum];
for (int i = 0; i < notes. Length; i++)
{
newNotes[i] = notes[i];
}
}
this. Name = name;
this. Notes = notes;
this. Rhythm = rhythm;
this. ScaleName = scaleName; } }
Он лишь описывает структуру мелодии, о которой мы говорили. Главное значение для нас будет иметь новый класс, который называется MelodyPlayer, который будет полностью отвечать за воспроизведение мелодии. Таким образом алгоритм наших действий таков:
Получаем на входе стиль (гамму); Генерируем последовательность нот по ней; Генерируем ритм; Формально компонуем это в мелодию; Получаем на входе темп и тонику; Теперь всё готово для воспроизведения.В итоге, для воспроизведения нужны следующие данные: тоника, темп, стиль, ноты, ритм.
Приведем сигнатуру метода воспроизведения мелодии
PlayMelody(SoundDevices sd, Melody melody, Note tonica,
double duration, Note[,] grifNotes, Button[,] buttons, CancellationToken ct);
SoundDevices – объекты для воспроизведения (каналы, выходные аудиоустройства),
grifNotes и buttons нужны для подсвечивания проигрываемых нот,
CancellationToken необходим для прерывания потока, если пришло сообщение извне.
Как видно, в параметрах нет переменной темпа. Но за темп отвечает переменная duration. Коэффициент темпа только и нужен для того, чтобы рассчитать продолжительность звучания всех нот и пауз. Тогда мы получим абсолютные значения этих продолжительностей.
Воспроизведение мелодии происходит в отдельном потоке для того, чтобы оно не мешало взаимодействию пользователя с окном программы. Основной поток должен всегда быть в режиме ожидания пользовательских действий. О том, как устроено разделение на два потока и о проблемах, возникающих при разделении, мы поговорим в параграфе 4.2.
Для воспроизведения ноты в виде звука мы использовали готовый проект. В нем представлено множество классов, которые предназначены для воспроизведения нот стандартными средствами операционной системы: в виде MIDI звуков.
Вначале происходит настройка среды: создание необходимых объектов и выбор инструментов.
outputDevice = ExampleUtil. ChooseOutputDeviceFromConsole();
Выбрали выходное устройство из списка доступных на данный момент.
outputDevice. Open();
outputDevice. SendProgramChange(Channel. Channel1, Instrument. AcousticGuitarSteel);
Здесь мы выбрали инструмент для проигрывания нот. В данном случае это акустическая гитара с металлическими струнами. На деле звук не оправдывает наших ожиданий. Но в данной работе качество звука не играет большой роли. Мы не создаем гитарный симулятор. Для нас важна сама суть создания мелодии.
OutputDevice – это объект, реализующий наше аудио-взаимодействие с внешней средой. С помощью него мы посылаем сигнал для проигрывания ноты, сигнал для полного затухания звучания всех нот. Вот примеры вызова самых распространенных методов:
outputDevice. SendNoteOn(channel, note, 80) –
посылаем сигнал для звучания ноты note в канад channel,
outputDevice. SendNoteOff(channel, note, 80) –
посылаем сигнал для затухания ноты note,
outputDevice. SilenceAllNotes() –
сигнал для затухания всех звучащих нот.
|
Из за большого объема этот материал размещен на нескольких страницах:
1 2 3 4 5 6 7 8 9 10 11 |


