Pull to refresh

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

Reading time8 min
Views15K
Не так давно я загорелся идеей написать свою 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», у которого я и подсмотрел таблицу аккордов. Это мне сэкономило чуточку времени.
Tags:
Hubs:
Total votes 25: ↑23 and ↓2+21
Comments29

Articles