We type in LilyPond using the midi-keyboard
Many note editors, including Finale and Sibelius, have the ability to type notes from the midi keyboard in two ways: either you can play something with a metronome, and it will be immediately recorded with notes, or you can only enter notes from it, and the rhythm and everything else is entered in the usual way.
I decided that a similar opportunity would not hurt for my preferred lilypond'a. Since the ability to write a midi file and then convert it using midi2ly does not suit me - too much information of just typing type cannot be reflected in a midi file (we already discussed this) - I decided to write a program so that the pressed keys and chords are immediately converted to the desired format.
UPD: A note of note is needed for about half of the following.
Set in lilypond using the \ relative command allows you to not specify an octave for each note, but to indicate only the direction of the octave change. Without such indications, each subsequent note (in the case of a chord, the sound indicated first) is no further than a quart. That is, the fa after the before will be scored higher, even if the f is sharp, and the before is flat (doubled, but a quart, yes).
Writing a program confronted me with the difficulties of two kinds: actually working with a midi keyboard and converting the received signals to the desired form.
midi-dot-net: work with MIDI
The first thing I did was download the midi-dot-net library . It provides the possibility of both input and output, but we are now interested in input.
List of devices, opening and closing
The list of available input devices, I stuffed it into the combo box.
using Midi;
// .......
private void LoadMidiDevices()
{
foreach (InputDevice d in InputDevice.InstalledDevices)
{
DeviceList.Items.Add(d.Name);
}
DeviceList.SelectedIndex = 0;
UpdateDevice();
}
In addition to the device name, you can also find out ManufacturerId, ProductId and all this together in one Spec field.
The Open () and Close () methods open and close the device, the state can be obtained from the IsOpen, StartReceiving (), StopReceiving (), IsReceiving fields, respectively, responsible for receiving information. The library provides convenient event binding in C # style.
The StartReceiving () method optionally accepts an object of type Clock, intended primarily for deferred MIDI output. If you pass null, then timestamps in events will be counted from the moment StartReceiving () is called.
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);
}
}
Do not forget to close the device when exiting
private void SettingsWindow_FormClosing(object sender, FormClosingEventArgs e)
{
d.StopReceiving();
d.Close();
}
Click Processing
I chose the operation algorithm as such: when you release all the keys, those that were pressed for more than 50 ms are transferred in a whole to the converter and sent to the active window using SendKeys. This makes it possible to type individual notes and chords.
The implementation is simple to disgrace:
private List notes;
private Dictionary 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();
}
}
}
}
I also added the ability to set leagues (parentheses in lilypond syntax) using the sustain pedal (and in my case, buttons) sustain. Since my keyboard sends ControlChange in two pieces, I added an extra check.
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;
}
The library also allows you to handle ProgramChange and PitchBend events.
We bite nuts of the elementary theory of music
The second task was more difficult than the first. I added a counter in the window to indicate the number of characters and a major-minor switch, and began to think.
First of all, I created an intermediate link between the Midi.Pitch value and the conclusion - the gamma step class (too lazy to look in the dictionary, first called it Grade, but Degree was necessary). Depending on the key, we will convert Midi.Pitch to Degree.
Arrays store the settings for converting the number in the chromatic (12-step) scale to the step of the 7-step scale and its alteration (increase or decrease).
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);
}
After that, the most unpleasant thing begins: to find out which note and with what sign will indicate the step with this number and with this alteration in the given tonality.
The method of the Degree class works like this. The fs variable contains the minimum required number of characters in the key , which is necessary so that this step in this key in an unalterated form has an alteration sign. The Number variable stores the number of the step, taking into account the octave, numMod - without taking into account.
Magically (adding four times the number of key characters and subtracting two in the case of minor), the step number turns into a note number in the white-key scale, and if the fs variable indicates the need, we add a “native” tonality alteration.
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 "";
}
}
The rest is simple: the need to add signs of changing the tonality is calculated by the step subtraction operator, and the last height is stored in the lastPitch field:
// 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 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);
}
conclusions
Complexity lay in wait for me not in the area where I was not sure, but in what was the subject of my specialty. But now I better understand how difficult it is for children in a music school :-). And getting notes became really faster.
https://github.com/m0003r/LilyInput