Не так давно я загорелся идеей написать свою 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», у которого я и подсмотрел таблицу аккордов. Это мне сэкономило чуточку времени.