Я уже пару раз писал про lilypond, а теперь я купил midi-клавиатуру.
Многие нотные редакторы, в том числе Finale и Sibelius, имеют возможность набора нот с midi-клавиатуры аж двумя способами: или можно сыграть что-нибудь под метроном, и это будет немедленно записано нотами, либо можно вводить с оной только ноты, а ритм и всё прочее вводится обычным способом.
Я решил, что аналогичная возможность не помешала бы и для предпочитаемого мною lilypond'а. Так как возможность записать midi-файл, а потом преобразовать его с помощью midi2ly меня не устраивает — слишком много информации именно нотонаборного толка в midi-файле отражены быть не могут (мы об этом уже дискутировали) — я решил написать программу для того, чтобы нажатые клавиши и аккорды немедленно преобразовывались в необходимый формат.
UPD: Нотная грамота нужна примерно для половины нижеследующего
Набор в lilypond с помощью команды \relative позволяет не указывать октаву для каждой ноты, а указывать только направление смены октавы. Без таких указаний каждая следующая нота (в случае аккорда — звук, указанный первым) оказывается не дальше, чем на кварту. То есть фа после до будет набрано выше, даже если фа — диез, а до — бемоль (дважды увеличенная, но кварта, да).
Написание программы поставило передо мной сложности двух родов: собственно работа с midi-клавиатурой и преобразование полученных сигналов в нужный вид.
Первым делом я скачал библиотеку midi-dot-net. Она предоставляет возможность как ввода, так и вывода, но интересует нас сейчас ввод.
Cписок доступных устройств ввода, я его запихнул в комбобокс.
Кроме имени устройства можно также узнать ManufacturerId, ProductId и всё это вместе в одном поле Spec.
Методы Open() и Close() открывают и закрывают устройство, состояние можно получить из поля IsOpen, StartReceiving(), StopReceiving(), IsReceiving, соответственно, отвечают за получение информации. Библиотека предоставляет удобную привязку событий в стиле C#.
Метод StartReceiving() опционально принимает объект типа Clock, предназначенный, в первую очередь, для отложенного MIDI-вывода. Если передать null, то таймстампы в событиях будут отсчитываться с момента вызова StartReceiving().
Не забываем закрыть устройство при выходе
Алгоритм работы я избрал таковым: при отпускании всех клавиш те из них, которые были нажаты дольше 50 мс передаются всем скопом в конвертер и отсылаются активному окну с помощью SendKeys. Это даёт возможность набирать и отдельные ноты, и аккорды.
Реализация проста до безобразия:
Также я добавил возможность набора лиг (круглые скобки в синтаксисе lilypond'а) с помощью педали (а в моём случае — кнопки) sustain. Поскольку моя клавиатура присылает ControlChange по две штуки, я добавил дополнительную проверку.
Также библиотека позволяет обрабатывать события ProgramChange и PitchBend.
Вторая задача оказалась посложнее первой. Я добавил в окошко счётчик для указания количества знаков и переключатель мажор-минор и начал думать.
Первым делом я создал промежуточное звено между значением Midi.Pitch и выводом — класс ступени гаммы (поленившись посмотреть в словарь, назвал его сначала Grade, а надо было Degree). В зависимости от тональности мы будем преобразовать Midi.Pitch в Degree.
Массивы хранят настройки преобразования номера в хроматической (12-ступенной) гамме в ступень 7-ступенной гаммы и её альтерацию (повышение или понижение).
После этого начинается самое неприятное: выяснить, какая нота и с каким знаком будет обозначать ступень с этим номером и с этой альтерацией в данной тональности.
Метод класса Degree работает так. В переменную fs помещается то минимально необходимое количество знаков в тональности, которое нужно для того, чтобы эта ступень в этой тональности в неальтерированном виде обладала знаком альтерации. В переменной Number хранится номер ступени с учётом октавы, numMod — без учёта.
Магическим образом (прибавляя умноженное на четыре количество ключевых знаков и отнимая два в случае минора) номер ступени превращается в номер ноты в белоклавишной гамме, и, если переменная fs говорит о необходимости, добавляем «родную» альтерацию тональности.
Остальное просто: необходимость добавления знаков изменения тональности вычисляется оператором вычитания ступеней, а последняя высота сохраняется в поле lastPitch:
Сложность подстерегала меня не в той области, где я был не уверен, а в том, что является предметом моей специальности. Зато я теперь лучше понимаю, как сложно бывает детям в музыкальной школе :-). И набирать ноты стало действительно быстрее.
https://github.com/m0003r/LilyInput
Многие нотные редакторы, в том числе Finale и Sibelius, имеют возможность набора нот с midi-клавиатуры аж двумя способами: или можно сыграть что-нибудь под метроном, и это будет немедленно записано нотами, либо можно вводить с оной только ноты, а ритм и всё прочее вводится обычным способом.
Я решил, что аналогичная возможность не помешала бы и для предпочитаемого мною lilypond'а. Так как возможность записать midi-файл, а потом преобразовать его с помощью midi2ly меня не устраивает — слишком много информации именно нотонаборного толка в midi-файле отражены быть не могут (мы об этом уже дискутировали) — я решил написать программу для того, чтобы нажатые клавиши и аккорды немедленно преобразовывались в необходимый формат.
UPD: Нотная грамота нужна примерно для половины нижеследующего
Набор в lilypond с помощью команды \relative позволяет не указывать октаву для каждой ноты, а указывать только направление смены октавы. Без таких указаний каждая следующая нота (в случае аккорда — звук, указанный первым) оказывается не дальше, чем на кварту. То есть фа после до будет набрано выше, даже если фа — диез, а до — бемоль (дважды увеличенная, но кварта, да).
Написание программы поставило передо мной сложности двух родов: собственно работа с midi-клавиатурой и преобразование полученных сигналов в нужный вид.
midi-dot-net: работа с MIDI
Первым делом я скачал библиотеку midi-dot-net. Она предоставляет возможность как ввода, так и вывода, но интересует нас сейчас ввод.
Список устройств, открытие и закрытие
Cписок доступных устройств ввода, я его запихнул в комбобокс.
using Midi; // ....... private void LoadMidiDevices() { foreach (InputDevice d in InputDevice.InstalledDevices) { DeviceList.Items.Add(d.Name); } DeviceList.SelectedIndex = 0; UpdateDevice(); }
Кроме имени устройства можно также узнать ManufacturerId, ProductId и всё это вместе в одном поле Spec.
Методы Open() и Close() открывают и закрывают устройство, состояние можно получить из поля IsOpen, StartReceiving(), StopReceiving(), IsReceiving, соответственно, отвечают за получение информации. Библиотека предоставляет удобную привязку событий в стиле C#.
Метод StartReceiving() опционально принимает объект типа Clock, предназначенный, в первую очередь, для отложенного MIDI-вывода. Если передать null, то таймстампы в событиях будут отсчитываться с момента вызова StartReceiving().
private void UpdateDevice() { if (d != null) { if (d.IsReceiving) d.StopReceiving(); if (d.IsOpen) d.Close(); } d = InputDevice.InstalledDevices[DeviceList.SelectedIndex]; if (!d.IsOpen) d.Open(); if (d.IsReceiving) d.StopReceiving(); d.StartReceiving(null); if (d != null) { d.NoteOn += new InputDevice.NoteOnHandler(this.NoteOn); d.NoteOff += new InputDevice.NoteOffHandler(this.NoteOff); d.ControlChange += new InputDevice.ControlChangeHandler(this.ControlChange); } }
Не забываем закрыть устройство при выходе
private void SettingsWindow_FormClosing(object sender, FormClosingEventArgs e) { d.StopReceiving(); d.Close(); }
Обработка нажатий
Алгоритм работы я избрал таковым: при отпускании всех клавиш те из них, которые были нажаты дольше 50 мс передаются всем скопом в конвертер и отсылаются активному окну с помощью SendKeys. Это даёт возможность набирать и отдельные ноты, и аккорды.
Реализация проста до безобразия:
NoteOn // NoteOff
private List<Pitch> notes; private Dictionary<Pitch, float> events; //.... public void NoteOn(NoteOnMessage msg) { lock (this) { events[msg.Pitch] = msg.Time; } } public void NoteOff(NoteOffMessage msg) { lock (this) { if (events.ContainsKey(msg.Pitch)) { if (msg.Time - events[msg.Pitch] > 0.05) { notes.Add(msg.Pitch); } events.Remove(msg.Pitch); if ((events.Count == 0) && (notes.Count > 0)) { SendKeys.SendWait(" " + cons.Convert(notes)); notes.Clear(); } } } }
Также я добавил возможность набора лиг (круглые скобки в синтаксисе lilypond'а) с помощью педали (а в моём случае — кнопки) sustain. Поскольку моя клавиатура присылает ControlChange по две штуки, я добавил дополнительную проверку.
bool sustain; // ....... public void ControlChange(ControlChangeMessage msg) { if (msg.Control == Midi.Control.SustainPedal) { if ((msg.Value > 64) && !sustain) { sustain = true; SendKeys.SendWait("{(}"); } if ((msg.Value < 64) && sustain) { sustain = false; SendKeys.SendWait("{)}"); } } return; }
Также библиотека позволяет обрабатывать события ProgramChange и PitchBend.
Грызём орехи элементарной теории музыки
Вторая задача оказалась посложнее первой. Я добавил в окошко счётчик для указания количества знаков и переключатель мажор-минор и начал думать.
Первым делом я создал промежуточное звено между значением Midi.Pitch и выводом — класс ступени гаммы (поленившись посмотреть в словарь, назвал его сначала Grade, а надо было Degree). В зависимости от тональности мы будем преобразовать Midi.Pitch в Degree.
Массивы хранят настройки преобразования номера в хроматической (12-ступенной) гамме в ступень 7-ступенной гаммы и её альтерацию (повышение или понижение).
PitchToGrade()
private static int[] majorScale = { 0, 1, 1, 2, 2, 3, 3, 4, 5, 5, 6, 6 }; private static int[] majorAcc = { 0, -1, 0, -1, 0, 0, 1, 0, -1, 0, -1, 0 }; private static int[] minorScale = { 0, 1, 1, 2, 2, 3, 3, 4, 5, 5, 6, 6 }; private static int[] minorAcc = { 0, -1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1 }; private Degree PitchToGrade(Pitch p) { // с этой ноты начинается гамма int keybase = (keys * 7) % 12 - (isMajor?0:3); // поэтому в хроматической гамме от этой ноты наша будет int offset = ((int)p - keybase) % 12; // и смещение октавы по 7-ступенной системе int octave = (((int)p - keybase) / 12) * 7; int num, acc; if (offset < 0) offset += 12; if (isMajor) { num = majorScale[offset] + octave; acc = majorAcc[offset]; } else { num = minorScale[offset] + octave; acc = minorAcc[offset]; } return new Degree(num, acc); }
После этого начинается самое неприятное: выяснить, какая нота и с каким знаком будет обозначать ступень с этим номером и с этой альтерацией в данной тональности.
Метод класса Degree работает так. В переменную fs помещается то минимально необходимое количество знаков в тональности, которое нужно для того, чтобы эта ступень в этой тональности в неальтерированном виде обладала знаком альтерации. В переменной Number хранится номер ступени с учётом октавы, numMod — без учёта.
Магическим образом (прибавляя умноженное на четыре количество ключевых знаков и отнимая два в случае минора) номер ступени превращается в номер ноты в белоклавишной гамме, и, если переменная fs говорит о необходимости, добавляем «родную» альтерацию тональности.
resolveIn()
private static String[] Naturals = { "c", "d", "e", "f", "g", "a", "h" }; private static String[] Sharps = { "cis", "dis", "eis", "fis", "gis", "ais", "his" }; private static String[] Flats = { "ces", "des", "es", "fes", "ges", "as", "b" }; private static String[] DoubleSharps = { "cisis", "disis", "eisis", "fisis", "gisis", "aisis", "bisis" }; private static String[] DoubleFlats = { "ceses", "deses", "eses", "feses", "geses", "ases", "beses" }; public String resolveIn(int keys, bool isMajor) { int fAcc = Acc; int fs; int fNum; int numMod = Number % 7; fs = isMajor ? 6 : 3; fs = (fs - 2*numMod) % 7; if (fs <= 0) fs += 7; if (keys < 0) fs = 8 - fs; if (fs <= Math.Abs(keys)) fAcc += keys / Math.Abs(keys); fNum = (numMod + keys*4 - (isMajor ? 0 : 2)) % 7; if (fNum < 0) fNum += 7; switch (fAcc) { case -2: return DoubleFlats[fNum]; case -1: return Flats[fNum]; case 0: return Naturals[fNum]; case 1: return Sharps[fNum]; case 2: return DoubleSharps[fNum]; default: return ""; } }
Остальное просто: необходимость добавления знаков изменения тональности вычисляется оператором вычитания ступеней, а последняя высота сохраняется в поле lastPitch:
Convert() и всё прочее
// class Degree public static String operator -(Degree a, Degree g) { int o; o = a.Number - g.Number; o = (int)Math.Round((double)o / 7.0); if (o > 0) return new String('\'', o); if (o < 0) return new String(',', -o); return ""; } // class PitchConsumer public String Convert(List<Pitch> pitches) { Pitch localLast = lastPitch; String accum; if ((int)lastPitch == 0) lastPitch = pitches[0]; pitches.Sort(); if (pitches.Count == 1) { accum = PitchToString(pitches[0], lastPitch); lastPitch = pitches[0]; } else { lastPitch = pitches[0]; accum = "<"; foreach (Pitch p in pitches) { if (accum.Length > 1) accum += " "; accum += PitchToString(p, localLast); localLast = p; } accum += ">"; } return accum; } private String PitchToString(Pitch p, Pitch last) { Degree g, glast; String note; g = PitchToGrade(p); glast = PitchToGrade(last); note = g.resolveIn(keys, isMajor); return note + (g - glast); }
Выводы
Сложность подстерегала меня не в той области, где я был не уверен, а в том, что является предметом моей специальности. Зато я теперь лучше понимаю, как сложно бывает детям в музыкальной школе :-). И набирать ноты стало действительно быстрее.
https://github.com/m0003r/LilyInput
