Paper
Swara: A LilyPond Toolkit for Neovim
Active

Overview

A Tree-sitter grammar, a Rust language server and a Neovim plugin for LilyPond, the plain-text music engraver, built from the ground up for speed.

lilypondneovimtree-sitterrustluamusic

01.What Swara Is

LilyPond is a music typesetter. A score is written as plain text, and LilyPond turns that text into an engraved PDF and a MIDI file, the standard format that stores which notes play, when and for how long, so that a synthesiser can perform them. A short example shows what such a text looks like.

Code
\version "2.26.0"
\relative c' {
\time 4/4
c4 d e f |
g2 g |
}

The first line names the LilyPond version the file was written for, so that LilyPond's convert-ly tool can update the file when the syntax changes in a later release. \relative c' places each note in the octave closest to the note before it, starting near middle C. \time 4/4 sets four quarter-note beats to a bar. In c4 the letter is the pitch and the number is the length, 4 meaning a quarter note, and a note written with no number, such as the d after it, keeps the length of the note before. The | is a bar check, which makes LilyPond warn when the notes before it do not exactly fill a bar. g2 is a half note. From these two bars LilyPond produces a page of engraved notation and a MIDI file that plays them.

A score written this way is source code, and it gains from everything a code editor gives source code: colours that show the structure, mistakes marked while they are typed, completion of command names, and a quick way to build the result and look at it. Swara brings those to Neovim, a keyboard-driven text editor that runs in a terminal. It has three parts.

  • A Tree-sitter grammar for LilyPond, written new, which gives a score its colours, folds and indentation.
  • A language server written in Rust, which checks the score each time typing pauses.
  • A Neovim plugin that wires both in, opens the PDF in Skim, previews and plays a score, and creates guitar, piano and song lessons that print Indian sargam names and Bengali lyrics.

The name comes from Indian classical music, where swara (স্বর in Bengali, स्वर in Hindi) is the word for a note of the scale.

02.Why It Exists

Neovim 0.12 recognises .ly and .ily files as LilyPond but ships no highlighting for them, so a score opens as plain text in one colour. Swara's defining requirement is speed: colours and syntax errors have to follow every keystroke with no visible lag. Its design also names what would count as a failure even if every feature worked.

  • Lag while typing.
  • Skim, or any other window, taking the keyboard focus away from the editor.
  • The formatter changing a note, a duration, an octave mark, a comment or a line break. It changes spacing and nothing else.
  • Shipping any text licensed under the GPL, a licence explained under Source and Licence below.
  • Failing silently. A missing tool is reported once, in one line, and everything that does not need it keeps working.

03.How the Three Parts Fit Together

Tree-sitter is a library that reads source code and builds a syntax tree, a structure that records how the text breaks into parts: this brace opens a block of music, this word is a note, this # starts embedded code. The rules that say how LilyPond text breaks into those parts are the grammar. What sets Tree-sitter apart is incremental reparsing. After an edit it rebuilds only the parts of the tree the edit touched and reuses the rest, so the cost of a keystroke depends on the size of the change, not on the size of the file. In the grammar's own measurements a one-character edit in a 10,000-bar score reparses in 1.36 ms, against 26.8 ms to read the same file from scratch.

That difference matters because of how a screen works. A display refreshing 60 times a second draws a new frame about every 16.7 ms, so anything that has to keep pace with typing must finish inside roughly 16 ms or the colours visibly trail the cursor.

A language server is a separate program that an editor talks to through the Language Server Protocol (LSP), a standard set of messages such as "here is the new text of the file" and "here are the errors on these lines". Because the protocol is shared, one server can work with any editor that speaks it. Swara's server runs as swara lsp. The same swara program carries the grammar built in, so the plugin takes its colours from the very program the server runs from, and the separate nvim-treesitter plugin is not needed.

The plugin is written in Lua, the scripting language Neovim embeds, and runs inside Neovim. It finds or downloads the swara program, starts the server, and adds the commands for building, previewing, playing and creating lessons.

Table 1. The three parts of Swara

PartWritten inRuns
The grammarA JavaScript description that Tree-sitter turns into C, plus a hand-written C scannerBuilt into the swara program, or loaded by nvim-treesitter
The language serverRustAs swara lsp, a process Neovim starts and talks to
The pluginLuaInside Neovim

04.What the Language Server Checks

Opening a .ly or .ily file starts the colours, folds and indentation from Swara's grammar, and starts the language server, which does the following.

  • Checks the text after every edit, within a few milliseconds, for the mistakes the grammar can see: unclosed braces and other syntax errors, commands nothing defines, note names the file's note language does not have, and \include lines that name no file.
  • Runs LilyPond's own check once typing pauses, so LilyPond's errors and warnings appear on the lines they name without the file being saved.
  • Tidies the file on each save, and on request through Neovim's formatting command.
  • Completes LilyPond commands after a backslash, shows a command's description when the cursor rests on it, and jumps to where a variable is defined.
  • Lists the file's variables and its \score, \book and \bookpart blocks, so that a picker or an outline can jump between them.
  • Builds the PDF and MIDI on each save, opens the PDF the first time, and has Skim reload it after every later build.

The instant checks come from Swara's own grammar, which does not yet read every corner of LilyPond. Its test corpus holds 2,774 files that LilyPond itself accepts, and on 6 of them the grammar reports a syntax error that LilyPond does not. Three of the six define new syntax while LilyPond runs them, which no grammar can know in advance, and three use rare forms inside \with blocks and override values. None of the 114 published scores from the Mutopia Project, a library of public-domain scores typeset in LilyPond, shows one. Where the two disagree, LilyPond's own check is the one to trust. The grammar page gives the full comparison.

05.Why Opening a File Never Runs LilyPond

LilyPond has a programming language built in, Scheme, and anything written after a # in a score is read as Scheme. In the lesson settings below, #'bengali is a small Scheme value, a name. Scheme in a file can do anything a program can do, including run other programs and change files, with the permissions of the user who runs LilyPond.

So opening a file never runs LilyPond. The first run for a file comes only after typing in it, saving it or asking for a build, each a deliberate act on that file, the same as pressing compile. A downloaded piece opened only to be read is never run, and the auto_check setting turns the check while typing off altogether.

06.Lessons in Sargam and Bengali

Indian classical music names the notes of the scale with syllables: Sa, Re, Ga, Ma, Pa, Dha and Ni, together called sargam. Unlike the letter names C, D and E, these are relative. Sa is whichever note the singer or player takes as home, and the others are counted up from it. With Sa on C, the notes C, D, E, F and G are Sa, Re, Ga, Ma and Pa, written সা রে গা মা পা in Bengali script and सा रे ग म प in Devanagari.

A semitone is the smallest step on a piano, from one key to the very next, black or white, and an octave holds twelve of them. Seven names cover twelve semitones because five of the names come in two forms. Komal (flat) Re, Ga, Dha and Ni sit a semitone below their ordinary places, and tivra (sharp) Ma sits a semitone above. Swara writes them in Bhatkhande notation, named after the musicologist who devised it. A komal swara is underlined, tivra Ma has a short vertical stroke above it, and each octave below Sa's adds a dot beneath the syllable while each octave above adds a dot over it.

A guitar or piano lesson prints the sargam name of every note under the staff. Two lines near the top of the lesson choose where Sa stands and which script to print.

Code
\swaraSa c'
\swaraScript #'bengali

\swaraSa c' puts Sa on middle C, which is also what it is when the line is left out. In LilyPond each ' after a note raises it one octave, so c is the C below middle C and c' is middle C itself. \swaraScript takes bengali, devanagari or latin, the last printing Sa, Re, Ga in Latin letters. Both settings hold for the whole file, and when either appears twice the later one wins.

:Swara new guitar followed by a name writes a guitar lesson with four staves stacked over one another. The top staff is ordinary notation in the guitar's treble clef, which sounds an octave below where it is written. The second is tablature, a staff of six lines, one per guitar string, where a number on a line says which fret to press on that string, here in standard tuning. The third prints each note's English name and the fourth its sargam name. The template starts with two bars, c4 d e f and g a b c, so the sargam line opens on সা রে গা মা.

A song lesson takes lyrics typed in English letters and prints them in Bengali script. gan becomes গান (song) and pakhi becomes পাখি (bird). Where one English letter could stand for two Bengali sounds, a capital letter picks the second: t gives ত and T gives ট, so Taka becomes টাকা (money), and i gives the short vowel sign ি while I gives the long one ী, so nodI becomes নদী (river). The typed words print in smaller type beneath the Bengali, or either line can print alone. The converter is a general engine, and Bengali is the one script it carries a table for so far.

07.Speed

Each figure below is a median: the runs are sorted and the middle one reported, so a single slow run cannot drag the result. Every target was met with LilyPond 2.26.0 on the machine described in the speed basis note, and the speed report gives the method with the slowest and fastest run of each measurement.

Table 2. Swara's measured speed against its targets

MeasurementMedianTarget
Colours after a keystroke0.19 msunder 16 ms
Instant checks after an edit1.2 msunder 50 ms
LilyPond check after the last edit, 100-bar song551 msunder 600 ms
Tidy, 100-bar song1.1 msunder 10 ms
Completion after a backslash1.1 msunder 20 ms
Server ready2.3 msunder 100 ms
PDF after save, 4-bar lesson266 msabout 300 ms
PDF after save, 100-bar song1,311 msabout 1,500 ms
Play, first time, to the first noteabout 1,060 msabout 1,500 ms
Play, later, to the first noteabout 4 msunder 200 ms

Of the 551 ms for the LilyPond check, 330 ms is the pause Swara waits for typing to stop, and most of the rest is LilyPond engraving the score. The two playback figures run to the moment the first note is sent to FluidSynth. The further delay inside FluidSynth's audio buffer was not measured.

08.Known Limits

Swara is at version 0.1.1, and its release notes list what it does not yet do.

  • A ~ inside a Bengali word being converted prints as a literal tilde.
  • Bengali is the only script the lyric converter has a table for.
  • Only LilyPond 2.26.0 is tested.
  • The Windows archive is built but untested. The tests run on macOS and Linux.
  • The delay inside FluidSynth's audio buffer is unmeasured.
  • The grammar reports a false syntax error on 6 of the 2,774 corpus files, as described above.

09.Source and Licence

Swara lives in three repositories, all under the MIT licence, a permissive licence that lets anyone use, change and share the code as long as the copyright notice stays with it.

swaraMIT tree-sitter-lilypondMIT homebrew-tapMIT

LilyPond itself is licensed under the GNU General Public License (GPL), which requires any work that copies GPL text to be released under the GPL as well. That is why the command descriptions Swara shows in completion and on hover never ship with it. They are LilyPond's own text, so the server reads them from the installed LilyPond into the user's cache instead. Nothing is copied from LilyPond, Frescobaldi, python-ly or nvim-lilypond-suite, and the grammar, its queries and its tests were written from scratch.

— Anirban

Hardware–software at the seam where compilers meet microcode.

FeedbackBook modePaper