Skip to content

Parse real-time MIDI input data and transform into lilypond notes

Notifications You must be signed in to change notification settings

niveK77pur/lilypond-midi-input

Repository files navigation

Real-Time MIDI to LilyPond Notes

Standalone tool reading input from a MIDI device and converting them into LilyPond notes, with integration into other tools as a strong focus.

About

This is a tool specifically targeted at writing LilyPond scores. Entering notes using a MIDI keyboard is very handy and can greatly speed up the process, for which I always used Frescobaldi. There was an issue however — I already had a fully personalized setup for writing LilyPond in my text editor of choice, yet always found myself going back to Frescobaldi for the MIDI input; as a result, I ended up writing my scores in Frescobaldi, even beyond the MIDI input. (Frescobaldi is great though!)

lilypond-midi-input aims to bridge the gap between MIDI input for LilyPond notes, and any arbitrary text editor which supports async inputs. The idea is that this tool will listen for MIDI inputs from a device, and will transform them into corresponding LilyPond notes that can directly be inserted into your LilyPond files!

This is a standalone program which does just that: Read MIDI inputs from a device, and spit out LilyPond notes onto stdout. This will hopefully make integration into other editors easier. Basic usage walks through how the program works. For those wishing to integrate this into their editors, please take a look at the specifications on how to handle the input and output streams.

Non-goals

Fully automate text input for LilyPond notes is not an objective for this tool. This means for example that adding note durations will not be handled here. Automatically detecting rhythm during playback is therefore also not an objective of this tool. Such features should be provided/created by wrappers.

Again, the main goal here is to provide translation of MIDI notes into LilyPond notes, and as a result make MIDI input easier to integrate into other editors.

Features

  • All notes on a keyboard are translated to LilyPond notes with absolute octave entry

    Demo Video 🎬

    A chromatic scale being played across the entire piano, with their corresponding LilyPond notes being output.

    notes-to-lilypond-feature.mp4
  • Specify musical key signatures to influence how accidentals (black keys) are interpreted

    Demo Video 🎬

    Shows the following keys

    • C major

    • A minor (harmonic minor), note the G sharp note

    • B major, note all black keys being sharps

    • G sharp minor (harmonic minor), note the G natural being output as F double-sharp

    • C flat major, note all black keys being flats

    • B flat minor (harmonic minor)

    musical-key-feature.mp4
  • Specify how to handle accidentals outside a key signature (fall back to sharps or flats)

    Demo Video 🎬
    • Example in F major which has a B flat

      accidentals-fM-feature.mp4
    • Example in G major which has an F sharp

      accidentals-gM-feature.mp4
  • Different input modes

    • Single: Input one note at a time

      Demo Video 🎬
      • Shows a scale being played

      • Shows a chord being played and how it inserts only single notes (even if all are held)

      • Shows long held notes to highlight that notes are inserted as soon as key is pressed

      mode-single-feature.mp4
    • Chord: Allow inputting chords by holding down multiple keys at once

      Demo Video 🎬
      • Shows a chord being played and how it is inserted after releasing the keys

      • Shows notes being held, while pressing new ones and releasing others, highlighting that notes will be aggregated until everything is released

      • Shows long held notes to highlight notes are inserted as soon as all keys are released

      mode-chord-feature.mp4
    • PedalChord: Behave like Chord when any piano pedal is pressed, otherwise behave like Single

      Demo Video 🎬
      • Shows chord being played without pedal, behaving like Single

      • Shows chord being with pedal, behaving like *Chord

      mode-pedal-chord-feature.mp4
    • PedalSingle: Behave like Single when any piano pedal is pressed, otherwise behave like Chord (the opposite of how PedalChord behaves)

      Demo Video 🎬
      • Shows chord being played without pedal, behaving like Chord

      • Shows chord being played with pedal, behaving like Single

      mode-pedal-single-feature.mp4
  • Specify custom alterations for notes within a scale/octave

    Demo Video 🎬
    • Shows every C being replaced by YO

    • Shows every B being replaced by BYE

    alterations-feature.mp4
  • Specify custom alterations across all notes of the MIDI device

    Demo Video 🎬
    • Shows one specific C being replaced by YO

    • Shows one specific B being replaced by BYE

    global-alteration-feature.mp4
  • List all available MIDI input devices

  • Specific handling of input/output for integration into other editors

    • stdout for relevant ouptut

    • stderr for sharing messages from the tool

    • stdin to asynchronously take options to change settings on-the-fly

Installation

You will need PortMidi installed, regardless of the installation method (well, except for Nix). Note the libportmidi-dev package should only be needed for Ubuntu when building from source.

pacman -S portmidi # for arch
apt install libportmidi0 libportmidi-dev # for debian/ubuntu

Pre-built binaries

The latest release will contain pre-built binaries (different versions due to the PortMidi system library).

Note
Be sure to make the binaries available as lilypond-midi-input on your system, without the _* extension. That one was only useful to distinguish the different versions in the release assets.

Build from source

You will need cargo and PortMidi installed to build the project. The binary will be installed as lilypond-midi-input.

cargo install --path . # inside this repository

Nix

This project also comes with a flake.nix, meaning that you can use this program without any additional hassle. For example, with flakes you can add it as follows to a dev shell.

{
  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
    utils.url = "github:numtide/flake-utils";
    lmi.url = "github:niveK77pur/lilypond-midi-input";
  };

  outputs = {
    nixpkgs,
    utils,
    lmi,
    ...
  }:
    utils.lib.eachDefaultSystem (system: let
      pkgs = nixpkgs.legacyPackages.${system};
    in {
      devShell = pkgs.mkShell {
        packages = [
          lmi.defaultPackage.${system}
        ];
      };
    });
}

Basic Usage

A comprehensive overview of settings and features can be found using the help page. More information can be found in a later section.

lilypond-midi-input --help

First, you need to specify which MIDI input device this tool should listen to. You can use the following command to get a list of available input devices. Take note of the name for the device of interest, we need to give it to the program to actually run it.

$ lilypond-midi-input --list-devices
1) Input: Midi Through Port-0
3) Input: USB-MIDI MIDI 1
4) Input: out

Let’s say we are interested in the input device listed as number 3 here. You can finally run the tool as follows.

lilypond-midi-input "USB-MIDI MIDI 1"
Note
The name must be an exact match! Leading and trailing spaces in the name are ignored.

To exit, you can simply press Ctrl+C.

Providing Options

As indicated by the --help page, you can pass various options via command line flags, which shall not be elaborated on further. It should be mentioned that using command line flags will set the options on start-up and also provides a bit more helpful error messages if arguments are invalid.

The next method discussed will launch the program (with its default values), and allow changing options later. Practically speaking, there really is no major difference between the two methods. If your editor cannot write to this program’s stdin stream, you can use these flags as a workaround to relaunch with new settings.

Changing options

This tool also allows changing/setting the options on-the-fly without restarting the program. To do this, you can directly type into the program’s stdin! Meaning that while the program is running, you can simply type commands into the terminal.

Upon successful parsing and execution of the given setting, the program will write a message to stderr, either indicating success or possibly indicating errors. As far as possible, the program tries to inform what has happened (through stderr), as otherwise it is difficult to judge whether the provided settings in stdin where handled correctly or not.

All options here have long and short versions, which the latter are particularly useful when manually typing in the commands into the terminal. A list of options and their values can be found in a later section.

The settings are given in the following form. You can specify one option at a time, or you can provide multiple options at once. A key that takes nested key-value pairs has its value given as SUBKEY:SUBVALUE and are comma separated (without spaces). Here are some examples to hopefully clarify.

Note
Different options are space separated; so currently the values may not contain any spaces.
KEY1=VALUE1
KEY3=SUBKEY1:SUBVALUE1,SUBKEY2:SUBVALUE2
KEY1=VALUE1 KEY2=VALUE2
KEY1=VALUE1 KEY2=VALUE2 KEY3=SUBKEY1:SUBVALUE1,SUBKEY2:SUBVALUE2

Specifications for integration into editors

The interaction with this tool happens fully through stdin, stdout and stderr. Here is how each of these streams are used by this tool, allowing you to properly integrate it into your editor.

Managing the process

Spawning the process is ideally done by your editor, so that it can properly manage all the input and output streams.

Specifics on how to interact with each stream is of course dependent on the editor and its capabilities. You can have a look at existing integrations for some examples and inspiration.

Important
The tool is not capable of exiting by itself (i.e. there is no exit command for example). That said, you should try to kill the process in question, which should ideally be done by your editor.

stdin

As mentioned in Changing options, the stdin solely takes settings as key-value pairs. Upon successful parsing, the corresponding option will be set/updated internally. A corresponding message will also be written to stderr.

For options and their values, please check the following section; for usage examples please check the section Changing options.

Important
If the program is not responding to inputs being sent through stdin, it is possible that you have provided an invalid option which is simply not being parsed and captured. Or, it is possible that your editor also needs to add a newline at the end of the message, in order to trigger Rust to actually read the input line.

stdout

This stream should only output data relevant to the task at hand. In the case of --list-devices, it will be the list of devices. In the case of a normal execution, stdout will only have LilyPond notes printed as you input notes through your MIDI keyboard.

That said, stdout can be taken as-is. A user could for example be prompted to pick a MIDI device based on the output of --list-devices. Most importantly, during normal execution the outputted LilyPond notes can be taken as-is in order to have them inserted into your text editor.

stderr

This stream contains any other message/information that the tool wants to share but should not be taken as text input by the editor. Currently, this counts general information such as a startup message, and indications that values were updated correctly via stdin. In case an option via stdin was invalid, an error message will also be written to stderr.

Errors are printed using the echoerr! macro, while other information is printed using the echoinfo! macro, the definition of both are found in this file. They prefix each line with a !! and :: respectively. This allows your client/editor to filter the messages from stderr according to actual errors or simple information.

Providing a list of options to the user

The program also provides a --list-options flag, which lists all available values for a given argument to stdout. The options are space separated, and no particular effort is made towards providing a well typeset output (i.e. as a tabular); the editors should decide how to treat the information.

The first value in the line corresponds to the actual enum variant’s name in the Rust code. The second value corresponds to the primary string from which the variant can be created. All following values are additional strings — usually shorthands — which can also be used to describe an enum variant. (See also the table).

All the values (without any " or ') can be used as-is to set an option via stdin. The second value can be used to set options via the command line arguments.

Using this method to display choices in the editor should be preferred as it avoids hardcoding the values. Further, if values should change, be added, or removed, it will require no intervention in the editor, as this tool can list its own options.

Options

Command Line Arguments

All flags and the values they can take are shown when running the program with the --help flag. Thus, they will not be further discussed.

Of importance to point out are the values expected by --alerations and --global-alterations. Both of these take a list of comma-separated subkey-subvalue pairs, which are mentioned in a previous section. More concrete details are given in the table.

Options for stdin

The option keys are the exact same as the command line flags but without the leading dashes. There are a few additional shorthands though. Also, the values it can take are a bit more broad compared to what the command line flags allow. Some of the values also allow shorthands. The following table describes the current options and their values. See also Changing options for examples on how to actually set them.

Table 1. Options and values for stdin

Options

Values

Description

Example

Long

Short

key

k

Can take all strings and enum variant names in the list of available keysignatures

Affects how accidentals will be printed depending on the given key signature. In GMajor, an F♯/G♭ will always be printed as fis no matter the value of accidentals. This can be overridden by alterations.

k=BFlatMajor is equivalent to key=besM

accidentals

a

Can take all strings and enum variant names in the list of accidentals

How to print accidentals that are not within the musical key? In the key of FMajor, sharps will print a G♯ (gis), whereas flats will print an A♭ (aes).

a=sharps is equivalent to a=s

mode

m

Can take all strings and enum variant names in the list of input modes

How to handle MIDI input? Single will only read one single note at a time as they are pressed. Chord will print a LilyPond chord after all notes were released. PedalChord merges both, behaving like Chord when any of the three pedals are pressed, and behaving like Single when all pedals are released. PedalSingle inverts the behaviour.

mode=Pedal is equivalent to m=p

alterations

alt

Subkey-subvalue pairs. I.e. key:value or key1:value1,key2:value2,…​. The key must be an integer between 0 and 11 inclusive, the value is considered a string (may not contain spaces). Trailing ` or `-` in the value can be used to adjust the octave up or down respectively. Multiple consecutive trailing ` or - can be used to adjust multiple octaves.

Set custom alterations within an octave; overrides special considerations for key signatures. Ottavation marks are still being set here. The numbers indicate each note in an octave, starting from C=0, C♯=1, D=2, …​, B=11

0:hello,10:world will make every note C output hello and every B♭ output world, together with their LilyPond ottavations (' or ,). An alteration of 0:bis will make the note produced by pressing a C always one octave too high; this can be remedied by doing 0:bis-.

global-alterations

galt

Same as alterations, without the integer constraint, and without the ottavation adjustments. You can determine the integers through use of the flag which displays the raw midi events (see Basic Usage).

Set custom alterations over all MIDI notes; further overrides alterations and key signatures. The numbers indicate the MIDI value attributed to said note. No ottavation marks (' or ,) are applied.

60:hello will only make middle C print a hello.

previous-chord

pc

Colon (:) separated list of absolute LilyPond note strings. Or clear to unset the previous chord.

Explicitly specify a chord which will yield q upon repeating. Useful when jumping around the file, and the tool does or does not return q appropriately.

pc=c,:eis':g will set <c, g eis'> as the previous chord. pc=clear will unset/forget the previous chord. Also see demo video.

list

Long or short version of all other options. Alternatively all will list the all values.

Not exactly an option, but allows listing values for options. Useful to see what the current state is.

list=k or list=key list the currently set key signature. list=all will list the current values of all options.

Integrations

NeoVim

I have written my own Neovim plugin which uses this tool to allow inputting notes asynchronously using a MIDI keyboard in Neovim! It also follows Vim’s modal philosophy and only inserts notes in Insert mode, and allows replacing notes in Replace mode!

See also

TODO

  • ❏ Generate notes for relative octave entry

  • Repeated chords should return q

  • ✓ List all currently set (global) alterations

  • ✓ List all options for a setting (avoids hardcoding them into editors)

  • ✓ Simple screencast to show how this looks in action (under [features](#features))

  • ✓ Debug option/mode to see raw midi events

  • ✓ Specify ottavation for alterations (i.e. 0=bis will cause the note to always be one octave too high)