Как я MIDI-клавиатуру писал

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

Раз, раз-два-три…


Перед тем как писать клавиатуру нужно как-то научиться воспроизводить звук. Первое что приходит на ум использовать встроенный в систему синтезатор. Он и на каждом устройстве есть, и устанавливать ничего не нужно. В общем работает из коробки.
Программу я решил писать на C#. Поискав в гугле, я узнал, что .NET сам по себе не умеет работать c MIDI, но есть WinAPI функции для этого. Последующий поиск в итоге привел меня к библиотеке NAudio. С помощью неё мы и будем воспроизводить звуки.

Для воспроизведения какой либо ноты необходимо отправить определенное сообщение на MidiOut, с указанием канала воспроизведения, ноты и силы нажатия.
Так, например, можно воспроизвести ноту ля 3-ей октавы:

midiOut.Send( MidiMessage.StartNote( 57, 127, 0 ).RawData ) //id звука, сила нажатия (0-127), номер канала;


Но не все так просто, воспроизведение ноты нужно остановить, иначе она так и будет звучать.

midiOut.Send( MidiMessage.StopNote( 57, 0, 0 ).RawData );


Перед воспроизведением нужно открыть нужное нам MIDI устройство. Делается это простым созданием объекта MidiOut. В конструктор передается номер устройства, т.к. их может быть несколько.
Узнать их количество можно считав статическое свойство MidiOut.NumberOfDevices, а получить сведения об этом устройстве методом MidiOut.DeviceInfo, передав ему идентификатор синтезатора.

Помните ту странную цифру 57? Это идентификатор ноты. Нумерация начинается с 0, каждое следующее значение это увеличение тональности на полутон.
Зависимость воспроизводимой ноты от ID можно увидеть на таблице:

image

Ознакомившись со всей этой информации я написал класс для упрощения работы с NAudio:
Скрытый текст

internal struct Note
{
    public Note( byte oct, Tones t )
    {
        octave = oct;
        tone = t;

        id = 12 + octave * 12 + (int)tone;
    }

    public byte octave;
    public Tones tone;
    public int id;
}

public enum Tones
{
    A = 9, Ad = 10, B = 11, C = 0, Cd = 1,
    D = 2, Dd = 3, E = 4, F = 5, Fd = 6, G = 7, Gd = 8
}

class AudioSintezator : IDisposable
{
    public int PlayTone( byte octave, Tones tone )
    {
        // 12 полутонов в октаве, начинаем считать с 0-й октавы (есть еще и -1-ая)
        int note = 12 + octave * 12 + (int)tone;

        if( !playingTones.Contains( note ) )
        {
            // воспроизводим ноту с макс. силой нажатия на канале 0
            midiOut.Send( MidiMessage.StartNote( note, 127, 0 ).RawData );
            playingTones.Add( note );
        }

        return note;
    }

    public void StopPlaying( int id )
    {
        if( playingTones.Contains( id ) )
        {
			// Останавливаем воспроизведение ноты
            midiOut.Send( MidiMessage.StopNote( id, 0, 0 ).RawData );
            playingTones.Remove( id );
        }
    }


    MidiOut midiOut = new MidiOut( 0 );
    List<int> playingTones = new List<int>();

    public void Dispose()
    {
        midiOut.Close();
        midiOut.Dispose();
    }
}


А так же сделал пробный вариант MIDI-клавиатуры

image

Секс, наркотики и рок-н-ролл


Следующий этап это создание гитарного грифа и привязки нот к струнам и ладам.
Гитарный гриф устроен очень просто. Каждая струна в открытом положении издает звук с определенным тоном. Эта же струна, зажатая на определенном ладу, издает звук на некоторое количество полутонов выше (1 лад — 1 полутон).
Если открытая струна издает звук E4, то она же зажатая на втором ладу издаст звук F#4, а на 12-ом E5.
Алгоритм прост: берем ноту открытой струны, и увеличиваем её на определенное количество полутонов.

Для упрощения себе жизни я написал класс:
Скрытый текст

class Guitar
{
    public Guitar( params Note[] tune )
    {
        for( int i = 0; i < 6; ++i )
        {
            strs.Add( new GuitarString( tune[i] ) );
        }
    }

    public List<Tuple<byte, byte>> GetFretsForNote( Note note )
    {
        // 1-е значение номер струны (от 0 до 5), 2-е - номер лада ( 0 - открытая струна)
        List<Tuple<byte, byte>> result = new List<Tuple<byte, byte>>();

        byte currentString = 0;
        foreach( var str in strs )
        {
            var fret = str.GetFretForNote( note );

            if( fret != -1 ) // Если на этой струне можно сыграть заданную ноту
            {
                result.Add( new Tuple<byte, byte>( currentString, (byte)fret ) );
            }

            ++currentString;
        }

        return result;
    }

    public Note GetNote( byte str, byte fret )
    {
        return strs[str].GetNoteForFret( fret );
    }

    public void SetTuning( params Note[] tune ) // звучание открытых струн
    {
        for( int i = 0; i < 6; ++i )
        {
            strs[i].SetTune( tune[i] );
        }
    }

    List<GuitarString> strs = new List<GuitarString>();
}

class GuitarString
{
    public GuitarString( Note note )
    {
        this.open = note;
    }

    public void SetTune( Note note )
    {
        this.open = note;
    }

    public Note GetNoteForFret( byte fret )
    {
        return open + fret;
    }

    public int GetFretForNote( Note note )
    {
        int fret = -1; // -1 означает, что нельзя сыграть ноту на этой струне

        if( open <= note )
        {
            int octDiff = note.octave - open.octave;
            int noteDiff = note.tone - open.tone;

            fret = octDiff * 12 + noteDiff;
        }

        return fret;
    }

    Note open;
}



Вот что у меня получилось на этом этапе:

image

Трезвучия, септаккорды и прочие радости музыки


Этот этап тоже не особо сложен за исключением одного момента, который будет описан чуть позже.
Немного теории: аккорд это набор звуков определенных тональностей. Ноты эти взяты не абы как — они расположены с определенными интервалами. Интервалы измеряются в полутонах.
Аккорд ля минор, например, имеет интервалы 3,4, т.е. представляет последовательность нот: A, C, E (ля, до, ми)
Больше я рассказать про это не смогу, т.к. сам дилетант в музыке, консерваторий не оканчивал. И боюсь наговорить много чего лишнего и далекого от истины. Больше можно узнать на википедии.

Вернемся к программе.
Алгоритм распознавания следующий:
  1. Находим самый низкий звук, он и будет базой для аккорда (тоникой, если не ошибаюсь)
  2. Считаем интервалы между «соседними» нотами
  3. Сверяемся с заранее подготовленной таблицей интервалов

На этом можно бы было и закончить, если бы не одно но: гитарные аккорды не так просто устроены, как хотелось бы. Они содержат в себе множество звуков определенной гаммы. Например гитарный ля минор содержит уже 5 звуков, хоть это и те же ля-до-ми.

Это усложняет распознавание:
  • Создаем список из нот, убирая из него повторяющиеся звуки (до разных октав, например)
  • Пододвигаем их ближе, убирая расстояния в октаву, а то и несколько
  • Проверяем по таблице интервалов. Если не нашли совпадений, то идем дальше
  • Перебираем все возможные сочетания (перестановки) и сверяемся с таблицей.
  • Если до сих пор не нашли, то называем аккорд неизвестным.

Как вы уже, наверное, догадались я так же создал класс для упрощения работы с аккордами:
Скрытый текст


static class Chords
{
    public static ChordType[] chordTypes = new ChordType[]{
        new ChordType("мажорное трезвучие", "", 4,3),
        new ChordType("минорное трезвучие", "m", 3,4),
        new ChordType("увеличенное трезвучие", "5+", 4,4),
        new ChordType("уменьшенное трезвучие", "m-5", 3,3),
        new ChordType("большой мажорный септаккорд", "maj7", 4,3,4),
        new ChordType("большой минорный септаккорд", "m+7", 3,4,4),
        new ChordType("доминантсептаккорд", "7", 4,3,3),
        new ChordType("малый минорный септаккорд", "m7", 3,4,3),
        new ChordType("полуувеличенный септаккорд", "maj5+", 4,4,3),
        new ChordType("полууменьшенный септаккорд", "m7-5", 3,3,4),
        new ChordType("уменьшенный септаккорд", "dim", 3,3,3),
        new ChordType("трезвучие с задержанием (IV)", "sus2", 2,5),
        new ChordType("трезвучие с задержанием (II)", "sus4", 5,2),
        new ChordType("секстмажор","6", 4,3,2),
        new ChordType("секстминор", "m6", 3,4,2),
        new ChordType("большой нонмажор", "9", 4,3,3,4),
        new ChordType("большой нонминор", "m9", 3,4,3,4),
        new ChordType("малый нонмажор", "-9", 4,3,3,3),
        new ChordType("малый нонминор", "m-9", 3,4,3,3),
        new ChordType("нота",""),
        new ChordType("малая секунда", " - М2", 1),
        new ChordType("большая секунда", " - Б2", 2),
        new ChordType("малая терция", " - М3", 3),
        new ChordType("большая терция", " - Б3", 4),
        new ChordType("чистая кварта", " - Ч4", 5),
        new ChordType("увеличенная кварта", " - УВ4", 6),
        new ChordType("чистая квинта", " - Ч5", 7),
        new ChordType("малая секста", " - М6", 8),
        new ChordType("большая секста", " - Б6", 9),
        new ChordType("малая септима", " - М7", 10),
        new ChordType("большая септима", " - Б7", 11),
        new ChordType("октава", " - О", 12),
        new ChordType("малая нона", " - М9", 13),
        new ChordType("большая нона", " - Б9", 14) 
};

    public static string[] chordsBases = new string[] {
        "A","A#","B","C","C#","D","D#","E",
        "F","F#","G","G#"
    };

    public static string[] chordMods = new string[] {
        "","m","5+","m-5","maj7","m+7","7",
        "m7","maj5+","m7-5","dim","sus2","sus4",
        "6","m6","9","m9","-9","m-9"
    };

    private static int GetChordType( List<Note> tmp )
    {
        int[] intervals = new int[tmp.Count - 1];
        for( int i = 0; i < tmp.Count - 1; ++i )
        {
            intervals[i] = tmp[i] - tmp[i + 1];
        }

        int type = 0;
        foreach( var chordType in Chords.chordTypes )
        {
            if( Utils.CompareArrays( intervals, chordType.intervals ) )
                break;
            ++type;
        }
        return type;
    }

    public static void GetChord( List<Note> chordNotes, out Note BaseNote, out ChordType type )
    {
        List<Note> notes = PrepareNotes( chordNotes ); // Подготовка нот к распознаванию
        int typeIndex = GetChordType( notes ); // Попытка распознать аккорд

        if( typeIndex < chordTypes.Length ) //Если нашли
        {
            BaseNote = notes[0];
            type = chordTypes[typeIndex];
        }
        else
        {
            bool unknown = true;
            var possibleChord = new List<Note>( notes );

            // Осуществляем полный перебор
            foreach( List<Note> perm in Utils.GeneratePermutation( possibleChord ) )
            {
                // Убираем промежутки между нотами ( > 12 полутонов )
                for( int k = 1; k < perm.Count; ++k )
                {
                    if( perm[k].tone > perm[k - 1].tone )
                    {
                        perm[k] = new Note( perm[k - 1].octave, perm[k].tone );
                    }
                    else
                    {
                        perm[k] = new Note( (byte)(perm[k - 1].octave + 1), perm[k].tone );
                    }
                }

                typeIndex = GetChordType( possibleChord );

                if( typeIndex < Chords.chordTypes.Length )
                {
                    unknown = false;
                    break; // Мы нашли что нужно, выходим
                }
            }

            if( unknown )
            {
                throw new Exception( "неизвестный аккорд" );
            }
            else
            {
                BaseNote = possibleChord[0];
                type = chordTypes[typeIndex];
            }
        }
    }

    private static List<Note> PrepareNotes( List<Note> notes )
    {
        List<Note> tmp = new List<Note>();

        bool finded = false;
        for( int i = 0; i < notes.Count; ++i )
        {
            finded = false;
            var note = notes[i];
            for( int j = 0; j < tmp.Count; ++j ) //Ищем похожие тона в списке
            {
                if( note.tone == tmp[j].tone )
                {
                    finded = true;
                    break;
                }
            }

            if( !finded ) //Если такой тон еще не встречался
            {
                tmp.Add( note );
            }
        }

        // Если все ноты одинаковые, но разные по тональности
        if( tmp.Count == 1 && notes.Count > 1 ) 
            return notes;

        // "пододвигаем" ноты
        byte lowest = tmp[0].octave;
        var lowesTone = tmp[0].tone;
        for( int i = 0; i < tmp.Count; ++i )
        {
            if( tmp[i].octave > lowest )
            {
                if( Utils.CountOfTones( tmp[i].tone, notes ) > 1 )
                {
                    if( tmp[i].tone > lowesTone )
                    {
                        tmp[i] = new Note( lowest, tmp[i].tone );
                    }
                    else
                    {
                        tmp[i] = new Note( (byte)(lowest + 1), tmp[i].tone );
                    }
                }
            }
        }

        tmp = tmp.OrderBy( x => x.id ).ToList();
        return tmp;
    }
}



Финальный результат:

image

Полные исходные коды проекта доступны на GitHub.
Так-же хочу выразить благодарность товарищу Sadler, автору поста «Ovation. Таблица аккордов своими руками с помощью JS и HTML5», у которого я и подсмотрел таблицу аккордов. Это мне сэкономило чуточку времени.
Поделиться публикацией

Комментарии 29

    +1
    По самой низкой ноте — неверно, например у C самая низкая нота может быть G или E в зависимости от постановки. Кроме того, если мы возьмем Am, то его можно сыграть с дополнительным басом, получив аккорды Am/B или Am/C (дальше растяжки не хватит). Аналогично, в случае с E можно получить E/F или E/G.

    Впрочем, это тонкости. Если программа ориентирована на фанатов семиаккордной музыки, можете не заморачиваться.

    Повторяющиеся звуки убирать нельзя, потому что Am с выкинутыми оттуда двумя нижними струнами (те самые «повторяющиеся звуки») будет называться уже A5. А если у Dm повысить ноту на первой струне на тон — получится Dadd7, если я все правильно помню (могу тут ошибиться).

    В общем, тут без образования никуда.
      +1
      По терминологии. Зажатая струна издает звук не с тональностью, а с частотой. Можно сказать «издает определенный тон». А «издает тональность» говорить нельзя, тональность — это набор звуков, определенным образом организованных друг относительно друга. В остальном — то же самое.

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

      Правда, в последнем положении я могу ошибаться, если что меня поправят. У меня все-таки образование по классу фортепиано, а не гитары.
        0
        >Зажатая струна издает звук не с тональностью, а с частотой
        Спасибо, поправлю. Я вообще не силен в музыкальных терминах. Я гитарист-самоучка-любитель.

        >Аккорд, кстати, для гитары — это просто определенная постановка руки
        Вот здесь вы не совсем правы. Если поменять строй гитары, и попытаться сыграть тот же Am со стандартной аппликатурой, то ничего хорошего не выйдет. То же касается аккордов с баррэ.
        Это те же аккорды, что и для фортепиано, только с добавленными звуками из основного аккорда, но других октав.
          –1
          Аккорд, кстати, для гитары — это просто определенная постановка руки, она не имеет особого отношения к тем нотам, которые при этом звучат. Одни и те же ноты, сыгранные на разных струнах и в разных местах грифа, будут давать разные аккорды.

          Нет. Аккорды для гитары это именно набор нот. Поэтому для одного и того же аккорда всегда существует несколько возможных аппликатур. Звучат они, конечно, несколько по-разному, но как правило они взаимозаменяемы.
          0
          Вы ошибаетесь. Повторяющиеся звуки лишь дублируют звуки аккорда. Проще говоря те же ноты, но другой октавы.
          Если убрать у Am 2 «нижние» струны, то это уже будет квинта (судя по табулатуре A5), там всего 2 звука (A2 и E3, А3 не берем — это те же Ля, но другой октавы).
          0
          Вам недалеко и до Guitar Pro! Вот бы кто-нибудь изобрёл прогу, которая по композиции (хотя бы акустической) распознаёт хотя бы аккорды (в идеале табы конечно) — для тех, у кого нет муз. образования…
            +1
            Как раз распознать чистую мелодию в табы/ноты — я думаю проще чем аккорды наверное. Еще лет 15 назад я находил какую-то программу типа wav2midi. Иногда она даже что-то правильно распознавала. Поищите, может прогресс шагнул вперед, и такая программа существует.

            А вообще подобрать аккорды не так сложно. Я делаю это так. Включаю музыку и прямо сижу с гитарой и подбираю. Конечно такой метод работает если ваша гитара настроена и гитара в композиции тоже настроена правильно и стандартно.
            А что надо подобрать? Может смогу помочь. Иногда действительно аккорды некоторых композиций не могу найти в интернете или качество подбора там никуда не годится.
              0
              Не смог найти ни одного приличного таба (исключая трёхаккордных) Руки Вверх-Алёшка :( Правда есть табы, в которых выложено оригинальное фортепианное соло, но на гитаре нужно быть гигантом семипалым чтобы такое сыграть.
                0
                Бывает умельцы делают такие соло, по принципу «чтобы было». Если видели записи исполнения музыкантов, то видно, что зачастую они даже руку по грифу не смещают. Эту табулатуру можно транспонировать, и играть будет намного проще. Можете для этого программой из поста воспользоваться. На грифе, как и на клавишах можно зажимать\выбирать ноты (правая\левая кнопки мыши)
                  0
                  Ой… жуть какая. Меня хватило только на полпесни. Ну так а че, там и есть три аккорда. Какие тут могут быть табы? Берешь эти три аккорда и как-то их там украшаешь. Куплет арпеджио, припев боем/чесом. Один аккорд можно варьировать, т.е. вместо одного играешь несколько типа Am, Amsus2 или как там он называется. Когда меняется немного ноты на 1 или 2 сруне. В общем подключи фантазию.

                  ЗЫ, А лучше играй хорошую музыку ;)
                    0
                    Это же Сергей Жуков, Руки Вверх :) под него все знакомились в 90-е и самом начале 2000-х, за это и любят. Конечно, в современном понимании это может и жуть.
                      0
                      Ну я в 90-х почему-то слушал другую музыку. При чем тут современное понимание? Многая музыка 60-х и сейчас звучит вполне уместно. А группы типа Руки Вверх у меня вызывали недоумение и в 90-х.

                      Но я вас в чем-то понимаю, ностальгия — это единственное оправдание подобных песен. Конечно если под эту музыку произошли какие-то эротические переживания — то она особенная. Я вот например люблю Dr.Alban — One love и Snap — music is a dancer, которые звучали на дискотеках во время моего отрочества.
                        0
                        Сейчас послушаю! Да, наверное Руки вверх уместны для тех, кто чуть помладше вас.
                          0
                          Мне тоже такая музыка нравится, слышал как-то на сборке дискотеки 80-х. Расслабляет…
                            0
                            Да, но я не могу объективно оценить, она и правда ок, или у меня это просто ностальгия и приятные ассоциации.
                  +1
                  такая программа существует много лет и называется Celemony Melodyne, на выходе выдает midi или исправленный wav. по факту — мощнейший тональный редактор.
                  –1
                  Я правильно понял, что можно выбрать аккорд, и программа покажет, как сыграть его на фортепиано?
                  Полезная штука, я вот на гитаре играю, а хотелось бы научиться также с легкостью аккомпанировать на ф-но. Но я каждый аккорд мучительно подбираю, т.к. теории музыки не научен.
                    0
                    Да, вы правильно поняли. Можно выбрать из списка, а можно «набрать» на гитаре (правой кнопкой мыши)
                      –2
                      Зашибись. Теперь я знаю, как научиться играть на ф-но.
                        –1
                        ничему хорошему таким образом не научитесь.
                          0
                          У каждого свой метод. Мне лично классическое музыкальное образование не подходит. Я это уже проходил.
                            +1
                            вы, видимо, не понимаете, что такая расстановка нот не позволит получить нормальной аппликации на клавишах, и в дальнейшем принесет только проблемы. Но можете и дальше минусовать, ваше право.
                              0
                              Ну держите плюс, мне не жалко.
                              А в чем проблема с аппликацией? Почему если перенести ноты гитарного аккорда на фортепиано, не получится ничего хорошего? Я же не говорю, что я собираюсь монотонно молотить по клавишам то, что мне покажет данная программа. Но мне надо от чего-то отталкиваться.
                              Например на гитаре я знаю основные аккорды в нескольких позициях, и это позволяет мне неплохо играть. Хотите, запишу что-нибудь, а вы мне честно скажете, лажа это или нет?
                                0
                                я как то пробовал перекладывать аккорды на клавиши в guitar pro, не думаю что сабжевая программа это делает лучше. результат был плачевный.
                                я не сомневаюсь в вашей способности игры на гитаре, я говорю о том, что лучше читать ноты, научиться это делать не сложно, не сложнее чем на гитаре вам было научиться, и отталкиваюсь при всем этом на собственном опыте.
                                На гитаре и клавишах я сам самоучка, хотя в детстве закончил музыкалку… на… бояне )))
                    0
                    Русские музыканты нумеруют октавы всё-таки не так, как MIDI-программисты — разница составляет 3 октавы. В таблице номеров нот был бы не лишним ещё один столбец, просто для удобства: OctaveОктаваCC♯...-1—...0Субконтроктава...1Контроктава...2Большая...3Малая...4Первая......
                      0
                      Кажется, табличные тэги не работают :-(

                      Octave Октава         C   C♯ ...
                       -1    —              ...
                        0    Субконтроктава ...
                        1    Контроктава    ...
                        2    Большая        ...
                        3    Малая          ...
                        4    Первая         ...
                       ...
                        0
                        Обновил пост.
                      0
                      > Алгоритм прост: берем тональность открытой струны

                      У струны, хоть открытой, хоть закрытой, нет тональности.

                      Напишите «берем ноту» или «берем тон».
                        0
                        готово

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

                      Самое читаемое