Не так давно я загорелся идеей написать свою MIDI-клавиатуру. Позже к ней был прикручен гитарный гриф, она научилась распознавать аккорды и воспроизводить мелодии. Собственно об этом и будет этот пост.
Если вам интересно как программно воспроизводить звук через MIDI-синтезаторы, алгоритм распознавания гитарных аккордов, или же вы просто любите играть на гитаре или клавишных инструментах прошу под кат.
Перед тем как писать клавиатуру нужно как-то научиться воспроизводить звук. Первое что приходит на ум использовать встроенный в систему синтезатор. Он и на каждом устройстве есть, и устанавливать ничего не нужно. В общем работает из коробки.
Программу я решил писать на C#. Поискав в гугле, я узнал, что .NET сам по себе не умеет работать c MIDI, но есть WinAPI функции для этого. Последующий поиск в итоге привел меня к библиотеке NAudio. С помощью неё мы и будем воспроизводить звуки.
Для воспроизведения какой либо ноты необходимо отправить определенное сообщение на MidiOut, с указанием канала воспроизведения, ноты и силы нажатия.
Так, например, можно воспроизвести ноту ля 3-ей октавы:
Но не все так просто, воспроизведение ноты нужно остановить, иначе она так и будет звучать.
Перед воспроизведением нужно открыть нужное нам MIDI устройство. Делается это простым созданием объекта MidiOut. В конструктор передается номер устройства, т.к. их может быть несколько.
Узнать их количество можно считав статическое свойство MidiOut.NumberOfDevices, а получить сведения об этом устройстве методом MidiOut.DeviceInfo, передав ему идентификатор синтезатора.
Помните ту странную цифру 57? Это идентификатор ноты. Нумерация начинается с 0, каждое следующее значение это увеличение тональности на полутон.
Зависимость воспроизводимой ноты от ID можно увидеть на таблице:

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

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

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

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

Ознакомившись со всей этой информации я написал класс для упрощения работы с 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-клавиатуры

Секс, наркотики и рок-н-ролл
Следующий этап это создание гитарного грифа и привязки нот к струнам и ладам.
Гитарный гриф устроен очень просто. Каждая струна в открытом положении издает звук с определенным тоном. Эта же струна, зажатая на определенном ладу, издает звук на некоторое количество полутонов выше (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; }
Вот что у меня получилось на этом этапе:

Трезвучия, септаккорды и прочие радости музыки
Этот этап тоже не особо сложен за исключением одного момента, который будет описан чуть позже.
Немного теории: аккорд это набор звуков определенных тональностей. Ноты эти взяты не абы как — они расположены с определенными интервалами. Интервалы измеряются в полутонах.
Аккорд ля минор, например, имеет интервалы 3,4, т.е. представляет последовательность нот: A, C, E (ля, до, ми)
Больше я рассказать про это не смогу, т.к. сам дилетант в музыке, консерваторий не оканчивал. И боюсь наговорить много чего лишнего и далекого от истины. Больше можно узнать на википедии.
Вернемся к программе.
Алгоритм распознавания следующий:
- Находим самый низкий звук, он и будет базой для аккорда (тоникой, если не ошибаюсь)
- Считаем интервалы между «соседними» нотами
- Сверяемся с заранее подготовленной таблицей интервалов
На этом можно бы было и закончить, если бы не одно но: гитарные аккорды не так просто устроены, как хотелось бы. Они содержат в себе множество звуков определенной гаммы. Например гитарный ля минор содержит уже 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; } }
Финальный результат:

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