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

Мелодии на бипере

  1. Выделяем логику воспроизведения в отдельный метод
  2. Создаем понятный формат
  3. Пример мелодии
  4. Код обработки формата
Идею для статьи навеяла одноименная тема на форуме. Практическое применение проигрывания мелодий с помощью Console.Beep, по всей видимости, если и существует, то очень ограниченное, так что все нижеизложенное - не более чем развлечение. Ну, разве что можно давать консольным приложениям какое-то осмысленное звуковое сопровождение.

Итак, идея в том, чтобы, используя Console.Beep для воспроизведения звуков и Thread.Sleep - для выдерживания пауз, можно было воспроизводить мелодии. Здесь я порассуждаю о том, как можно улучшить инструментарий.


Выделяем логику воспроизведения в отдельный метод



Первое, что можно было сделать - это отделить мелодию от кода, который ее воспроизводит. Для этого введем простой текстовый формат, в котором просто будут через пробел написаны целые числа. При этом всю последовательность можно будет разбить на пары, где первое число будет частотой звука, а второе - длительностью. Для пауз частоту будем указывать как 0. Кроме того, для удобства оформления можно в качестве разделителей использовать не только одиночный пробел но и вообще последовательности пробельных символов( помимо пробела это табуляция, возврат каретки и новая строка). Напишем код, обрабатывающий такой текст.
Код csharp Выделить
        static void Play(string sounds)
        {
            var nums = sounds.Split(" \t\r\n".ToCharArray(), StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToArray();
            for (int i = 0; i < nums.Length; i += 2)
            {
                if (nums[i] == 0)
                {
                    Thread.Sleep(nums[i + 1]);
                }
                else
                {
                    Console.Beep(nums[i], nums[i + 1]);
                }
            }
        }
Теперь для воспроизведения мелодий, содержащихся в упомянутой теме можно написать такой код
Код csharp Выделить
            var song1 = "247 500 417 500 417 500 370 500 417 500 329 500 247 500 247 500 247 500 417 500 417 500 370 500 417 500 497 500 0 500 497 500 277 500 277 500 440 500 440 500 417 500 370 500 329 500 247 500 417 500 417 500 370 500 417 500 329 500";
            Play(song1);
            Thread.Sleep(1000);
            var song2 = "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";
            Play(song2);
Таким образом мы теперь можем разместить в ресурсах или еще где-нибудь любое количество мелодий в простом формате и воспроизводить их с помощью нашего метода по мере надобности. Кроме того, можно их сохранять в отдельные файлы и запускать с помощью нашего приложения, предварительно разместив в методе Main следующий код.
Код csharp Выделить
           var a = Environment.GetCommandLineArgs();
            if (a.Length > 1)
            {
                var path = a[1];
                Play(File.ReadAllText(path));
            }
Можно даже зарегистрировать в системе собственное расширение для этого файла, ассоциировать его с нашей программой и проигрывать двойным щелчком по файлу.

Создаем понятный формат



Главный недостаток изложенного выше подхода состоит в том, что мелодии в таком формате брать абсолютно неоткуда. При желании, вероятно, можно найти пару-тройку мелодий, подобранных энтузиастами, но вряд ли где-то существуют большие архивы таких мелодий. Стало быть возникает вопрос: откуда все это брать? Само собой, можно найти ноты практически любой известной мелодии, но как это превратить в последовательность чисел, представляющий частоту и длительности в миллисекундах? Собственно этим мы сейчас и займемся. Сразу скажу, что для того, чтобы можно было воспользоваться тем, что будет написано ниже, требуется базовое знание нотной грамоты, то есть как минимум надо понимать написанное в нотном листе(уметь это сыграть или спеть - необязательно, но понимать значение закорчюек - необходимо), если этого нет - дальше можно не читать.

Для того, чтобы мелодию можно было легко "снять с листа" нам понадобится формат, в котором звуки будут представлены нотами, а длительность указана в формате, соответствующем принятому в нотной записи. Поэтому для обозначения нот я использовал следующий формат:
ОктаваНотаАлтерацияДлительность

Значение элементов:
Октава обозначается цифрой от 0 до 5. Используются "голосовые" октавы и нумеруются как на фортепиано, кроме того для мелодий, записанных в скрипичном ключе иногда придется использовать некоторые звуки малой октавы, она будет обозначаться как 0.

Нота обозначается символами от a до g латинского алфавита (американская система).
a - ля
b - си
c - до
d - ре
e - ми
f - фа
g - соль

Альтерация может иметь следующие значения + - диез, ++ - дубль-диез, - - бемоль, -- - дубль-бемоль, = - бекар.

Длительность как и в нотной записи обозначается долями от длительности целой ноты. То есть для 1/4, 1/8, 1/16 мы будем иметь запись 4, 8 и 16 соответственно. Для триолей и квинтолей надо вычислить, какую долю целой ноты они представляют. Например триоль, записанная как восьмые ноты, фактически делит четвертую ноту на три части. Несложно подсчитать, что если 1/4 разделить на 3, получится 1/12. Соответственно эти ноты надо записывать как 12. Для составных размеров, которые получаются либо использованием точек, либо слиянием слигованных нот одной высоты можно использовать запись через запятую. То есть если нота из триоли-восьмушки слигованна с обычной восьмушкой с точкой, то длительность такой ноты будет равна 1/12+1/8+1/16, ну и соответственно записать это можно так 12,8,16.
Несколько примеров:
2c4 - до второй октавы, длительность - 1/4
1a-8,16 - ля-бемоль первой октавы, длительность 1/8 с точкой
0f=12,8,16 - фа-бекар малой октавы, длительность - нота триоли-восьмушки слигована с восьмушкой с точкой.

Паузы обозначаются буквой x латинского алфавита, за которой следует длительность в формате, описанном выше.

Тональность задается либо нулем, который обозначает тональности до-мажор или ля-минор; либо двумя символами, первый из которых - цифра, второй - плюс или минус. Цифра обозначает количество ключевых альтераций, плюс - диезы, минус - бемоли. То есть, если тональность обозначена как 4+, значит на ключе четыре диеза, а если 3- - на ключе три бемоля.

В качестве разделителя тактов используется вертикальная черта. Он необязателен в большинстве случаев. Главное его назначение - отмена строчных альтераций предыдущего такта. Кроме того удобно писать все так же как в нотах, включая переносы строк и такты. Тогда будет проще ориентироваться в том случае, если что-то надо будет подправить. Разделители тактов необязательно ставить в точности там же, где они находятся в нотах, поскольку никакой ритмической нагрузки они не несут. Если нота начинается в одном такте, а заканчивается в другом, то разделитель(если он вообще нужен) можно поставить и после этой ноты.

Структура кода мелодии достаточно проста:
Вначале указываем тональность как описано выше, важно, чтобы это были первые символы строки, никаких пробелов вначале, пустых строк и т. д. Далее через пробел длительность целой ноты в миллисекундах(например 4000). Далее через пробел, собственно сам код мелодии. Кроме пробелов можно использовать и другие разделители, главное, чтобы они не были приняты за другие элементы кода. Элементы ищутся регулярными выражениями и все, что не соответствует формату элементов кода может быть истолковано ка разделители. Так что можно смело переносить все на новую строку, использовать дополнительные метки и т. д.

Пример мелодии



В качестве примера взял популярную песню, ноты которой найдены на просторах интернета(картинка во вложении). Кое что сократил, кое-что упростил, но не суть - это просто пример. Главное - мелодия осталась узнаваемой, что вполне достаточно для демонстрации. Собственно код мелодии
Цитата:
4+ 4000 1b8 1b16 | 2c12 1b12 1g12,8,16 1f16 | 1e2 | x2 | x4 x8 1e16 1e16 | 1e8 1e16 1f8,16 1e8 |
1e2 | x2 | x4 x8 2d16 2d16 | 2d8 2d16 2e8,16 2f16 2c16,4 x8 2c16 2c16 | 2c8 2c16 2d8,16 2e16 1b8,16 x8 x8 1g16 1g16 |
1a8 1g16 1a8,16 1b8 | 1b2 |

2f4 2e2 x2 2g8 2g16 2a8,16 2g8 2f8 2d16 2e8,16 x8 2g16 2g16 | 2a8 2g16 2f8,16 2e16 2g8,16 2f8 2e4 | 2g4 2f4 |
2f4 2e2 x2 2g8 2g16 2a8,16 2g8 2f8 2d16 2e8,16 x8 2g16 2g16 | 2a8 2g16 2f8,16 2e16 2g8,16 2f8 2e4 | 2g4 2f4 |
Здесь к сожалению нет строчных алтераций, так что их просто не тестировал.

Код обработки формата



Код парсера будет следующим
Код csharp Выделить
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
 
namespace Beepper
{
    class MusicFileParser
    {
        Dictionary<char, int> currentAlteratives = new Dictionary<char, int>()
        {
            {'a', 0}, {'b', 0}, {'c', 0}, {'d', 0}, {'e', 0}, {'f', 0}, {'g', 0}
        };
 
        Dictionary<char, int> notePosition = new Dictionary<char, int>()
        {
            {'a', 0}, {'b', 2}, {'c', -9}, {'d', -7}, {'e', -5}, {'f', -4}, {'g', -2}
        };
 
        const string sharpChars = "fcgdaeb";
        const string flatChars = "beadgcf";
        string key = "0";
        double fullNoteDuration = 4000.0;
        Regex noteAtom = new Regex(@"(?<octave>[0-5])(?<note>[a-g])(?<alterative>\+\+|--|\+|-|=)?(?<duration>\d+(,\d+)*)|(?<pause>x)(?<duration>\d+(,\d+)*)|(?<beat>\|)", RegexOptions.Compiled);
        Regex meta = new Regex(@"^(?<key>0|\d[+-])\s(?<fulldur>\d+)", RegexOptions.Compiled);
 
        void RefreshKey()
        {
            if (key == "0")
            {
                currentAlteratives.Keys.ToList().ForEach(c => currentAlteratives[c] = 0);
            }
            else if (key.Length == 2 && char.IsDigit(key[0]) && "+-".Contains(key[1]))
            {
                var plusOrMinus = key[1] == '+' ? 1 : -1;
                var alters = (plusOrMinus == 1 ? sharpChars : flatChars).Substring(0, key[0] - 48);
                currentAlteratives.Keys.ToList().ForEach(c => currentAlteratives[c] = alters.Contains(c) ? plusOrMinus : 0);
            }
            else
            {
                throw new FormatException("Строка, обозначающая тональность должна иметь значение \"0\" или состоять из двух символов, первый из которых - число, а второй - плюс или минус.");
            }
        }
 
        public string Parse(string notes)
        {
            var metaResult = meta.Match(notes);
            key = metaResult.Groups["key"].Value;
            fullNoteDuration = double.Parse(metaResult.Groups["fulldur"].Value);
            RefreshKey();
            var resultList = new List<string>();
            var noteMatches = noteAtom.Matches(notes);
            foreach (Match m in noteMatches)
            {
                if (m.Groups["beat"].Success) RefreshKey();
                else if (m.Groups["pause"].Success)
                {
                    var duration = CalculateDuration(m.Groups["duration"].Value);
                    resultList.Add("0");
                    resultList.Add(duration.ToString());
                }
                else
                {
                    var friquency = CalculateFriquency(m.Groups["octave"].Value, m.Groups["note"].Value, m.Groups["alterative"].Value);
                    var duration = CalculateDuration(m.Groups["duration"].Value);
                    resultList.Add(friquency.ToString());
                    resultList.Add(duration.ToString());
                }
            }
 
            return string.Join(" ", resultList);
        }
 
        int CalculateFriquency(string octave, string note, string alterative)
        {
            char noteChar = note[0];
            if (alterative.Length > 0)
            {
                int alt = 0;
                switch (alterative)
                {
                    case "+": alt = 1; break;
                    case "++": alt = 2; break;
                    case "-": alt = -1; break;
                    case "--": alt = -2; break;
                    case "=": alt = 0; break;
                    default:
                        break;
                }
                currentAlteratives[noteChar] = alt;
            }
            var distance = (double)((int.Parse(octave) - 1) * 12 + notePosition[noteChar] + currentAlteratives[noteChar]);
            double friq = 440.0 * Math.Pow(2.0, distance / 12.0);
            return (int)Math.Round(friq);
        }
 
        int CalculateDuration(string noteDur)
        {
            var dblResult = noteDur.Split(',').Select(d => fullNoteDuration / double.Parse(d)).Sum();
            return (int)Math.Round(dblResult);
        }
    }
}
Метод Parse переводит наш формат в формат, который понимает ранее описанный метод Play. Таким образом запустить мелодию можно так
Код csharp Выделить
            var vodka = @"4+ 4000 1b8 1b16 | 2c12 1b12 1g12,8,16 1f16 | 1e2 | x2 | x4 x8 1e16 1e16 | 1e8 1e16 1f8,16 1e8 |
                          1e2 |  x2 | x4 x8 2d16 2d16 | 2d8 2d16 2e8,16 2f16 2c16,4 x8 2c16 2c16 | 2c8 2c16 2d8,16 2e16 1b8,16 x8 x8 1g16 1g16 |
                          1a8 1g16 1a8,16 1b8 | 1b2 | 
 
                          2f4 2e2 x2 2g8 2g16 2a8,16 2g8 2f8 2d16 2e8,16 x8 2g16 2g16 | 2a8 2g16 2f8,16 2e16 2g8,16 2f8 2e4 | 2g4 2f4 |
                          2f4 2e2 x2 2g8 2g16 2a8,16 2g8 2f8 2d16 2e8,16 x8 2g16 2g16 | 2a8 2g16 2f8,16 2e16 2g8,16 2f8 2e4 | 2g4 2f4 |";
            var parser = new MusicFileParser();
            Play(parser.Parse(vodka));
Здесь воспроизведение произведений с многотональным изложением (как и многое другое, в принципе) - не предусмотрено. В крайнем случае можно куски для каждой тональности писать и парсить отдельно, а потом объединять. Но, поскольку вряд ли в этом возникнет острая необоходимость, все это я не стал включать в формат.

Все описанное есть в проекте во вложении.
Миниатюры
Вложения

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

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