Software » Max/MSP

LilyPond Music Notation inside Max/MSP

I often find it easier to understand what I'm hearing when I can see a visual representation of the music. This has become more important as I've been experimenting with generative music. I'm starting to give up full control over the composition process and it's not always clear what the results are without a lot of repeated listens.

Unfortunately, Max doesn't have much support for standard music notation. So I've been experimenting with generating notation inside Max using LilyPond, my jruby object, and Max's jweb object. This isn't a great solution: it can be slow and it only displays the notation. You can't interact with the notation in any way, which is what I'd really want. And you need to figure out how to work with Lilypond's syntax. But if you don't want to pay money or spend a ton of time writing your own notation software, this is potentially a viable solution for getting notation inside Max.

If you want to try this out, assuming you have Max installed already:

  1. Install JRuby for Max
  2. Install LilyPond
  3. Copy the text in this box (it's the patcher source code)
  4. In Max's menu, go to File → New from Clipboard
  5. Lock the patcher to interact with it
  6. Follow the instructions in the patcher (first load the Ruby script, then try evaluating some Lilypond syntax)

I'm not going to give a tutorial on LilyPond, so check out the documentation on their site if you want to play around with notating different things.

So what's going on under the hood? A Ruby script does most of the work. Ruby is certainly not the only solution here, I just like working with it. This code could be ported to C or Java. Let's take a look at what the Ruby script is doing:

LILYPOND = '/Applications/LilyPond.app/Contents/Resources/bin/lilypond'
FORMAT = 'png'  # 'pdf' also works

require 'tempfile'
INPUT = Tempfile.new('maxlily_input.ly').path
OUTPUT_ARG = Tempfile.new('maxlily_output').path
OUTPUT = "#{OUTPUT_ARG}.#{FORMAT}"


def notate(lilypond_syntax)
  out0 'hidden', 1 # hide jweb and display progress spinner

  File.open(INPUT, 'w') do |lilypond_input|
    lilypond_input.write %Q(
      \\include "english.ly"
      #{lilypond_syntax}
    )
  end

  Thread.new do
    command = %Q["#{LILYPOND}" -f #{FORMAT} -o "#{OUTPUT_ARG}" "#{INPUT}"]
    system command
    if $?.success?
      out0 'url', "file://#{OUTPUT}" # load the output in jweb
      out0 'hidden', 0 # show jweb
    else
      error "Lilypond conversion failed. Could not run:"
      error command
    end
  end
end


at_exit do # clean up temp files
  File.unlink INPUT if File.exists? INPUT
  File.unlink OUTPUT_ARG if File.exists? OUTPUT_ARG
  File.unlink OUTPUT if File.exists? OUTPUT
end

First some constants are setup:

LILYPOND = '/Applications/LilyPond.app/Contents/Resources/bin/lilypond'
FORMAT = 'png'  # 'pdf' also works

LILYPOND is the path to the lilypond executable command, which takes the Lilypond syntax as input and produces the notation graphics. This should be the default location for lilypond on OS X, but you may need to modify the path on your computer.

Format controls the output format. PNG images are nice and small. If you switch to PDF the quality is higher, and (at least on OS X) you can zoom into the PDF and open it in Preview for printing.

Next, we need some temp files to work with:

require 'tempfile'
INPUT = Tempfile.new('maxlily_input.ly').path
OUTPUT_ARG = Tempfile.new('maxlily_output').path
OUTPUT = "#{OUTPUT_ARG}.#{FORMAT}"

The lilypond command expects the input syntax to be in a text file. We'll write this text file to the INPUT temp file. OUTPUT_ARG is there for some weirdness with the lilypond command. You need to give it the output file without a file extension, which is OUTPUT_ARG. Lilypond will add the file extension automatically, and the actual output will be at OUTPUT.

When you click that "Evaluate" button in the Max patcher, it calls the notate() method in the Ruby script with the Lilypond syntax as the method's argument. This method does a few things:

It's important that a background thread is used. When notate() is called, Max waits while it runs. Inside notate(), executing the lilypond command can take a few seconds. If we didn't do anything to handle that delay, Max would hang duration that time and the UI would be unresponsive. That's a terrible user experience. So we ensure Max remains responsive by doing any long-running work in a background thread.

Finally, when at_exit is triggered (when you close the Max patcher or delete the jruby object), it deletes the temp files.

There you have it. This isn't an ideal solution to music notation in Max/MSP but it works. Next steps would be to write an automated converter between whatever music representation is being used and the lilypond format. A lot of tie you might not have an explicit music representation, or every patcher might be different, and writing the conversion to lilypond may be more trouble than it's worth. Still, this patcher can be a fun way to learn LilyPond syntax and play around with it in an interactive environment.



First written in 2008. Updated January 2014.