Melrōse, a language to program and play music

TL;DR

Melrōse is both a language and a tool to create and listen to music interactively, The language uses musical primitives (note, sequence, chord) and many functions (map, group, transpose) that can be used to create more complex patterns, loops and tracks. Melrōse uses MIDI output to produce sound by any (hard or software) device attached. Melrōse can also react on MIDI inputs to start, record and stop playing musical objects. A plugin is available for Microsoft Visual Studio for the best usage experience. For a quickstart, without any installation, you can use the Melrōse playground.

Introduction

Music composition is both a creative process and one that follows many rules (chords, scales, patterns). Exploring variations and combinations can be done by playing instruments. With the availabilty of many digital instruments, both soft- and hardware, it is also possible to create music using digital programming methods.

Melrōse offers a fully human-centric approach to algorithmic composition [1] of music. It defines a new programming language and includes a tool to play music by evaluating expressions in that language. With Melrōse, exploring patterns in music is done by creating musical objects that can be played and changed while listening.

Language

Note

The language design started by defining a number of musical primitives, objects that can be composed to larger complex ones. The first such primitive is a note and is a symbol to represent a sound. To create a note, you write this expression:

note('c')

You can also express the fraction (affects duration), octave, accidental (sharp,flat) and dynamic (forte, pianissimo) in one notation:

note('8a#3++')  // ⅛ note A, sharp, 3rd octave, forte

This is a compact notation that exactly describes multiple aspects of a musical note. The note name can be both lowercase (no shift key needed) and uppercase.

The two slashes // can be used to put comments in a script, at the end of an expression or in between expression lines.

Combining notes

Creating melodies by stitching together individual note objects is not practical as it requires lots of scripting to achieve simple melodies. Therefore, a second basic musical object is introduced, the sequence:

sequence('c e g')

This expression creates a sequence of notes in which each note is described using the same notation as used with the note function. The notes will be played in the order as it appears in the sequence using the current tempo (bpm). Additionally, a sequence one can also describe grouping of notes using parentheses:

sequence('(c e g) = (g c5 e5)') // C chord and its first inversion, separated by a quarter rest

Other basic musical objects that combine notes are: scale, chord, chordsequence and chordprogression.

Functions for properties

Recall the example of creating a note:

note('8a#3++')  // ⅛ note A, sharp, 3rd octave, forte

In this expression, all note properties (pitch,duration,velocity) are part of the input string. The main reason for having a programmable music language is to be able to modify all such properties using functions that manipulate them. Therefore, functions are added to the language such as transpose (changing the pitch), fraction (changing duration) and velocity (changing the loudness to change properties of musical objects. For example, the same note can also be expressed like this:

fraction(8, octave(-1, dynamic('++', transpose(1,note('a')))))

With this verbose structure, it is possible to change the properties of the note A by changing the first parameter of each function.

Let us write a loop that drives the pitch of a note.

a = note('a')
t = interval(-4,4,1) // from -4 to 4 by 1

loop( transpose(t,a), next(t))

Running this loop will play each argument starting with transpose(t,a) and then next(t). Because of the function, the note a will be transposed with t semitones ;the current value of t is -4. The next function will simplify increase t by 1.

Functions that change structure

Other functions exist in the language that change the structure of a sequence. Examples are resequence, map, replace and group. The purpose of these functions is to easily explore variations of a given sequence without copy-paste-modifying them.

(un)group

The group function will create a new music object from all the notes in a sequence.

group(sequence('c e g')) // (C E G) which is C chord
ungroup(chord('c')) // C E G, which are the notes in sequence

map

In general, a map takes notes of a sequence and applies some transformation on it. One such map function is octavemap.

octavemap('1:0 2:-1 3:1', sequence('A B C5')) // A B3 C6

Functions that create patterns

The last category of functions to explain here are the ones that create patterns. A typical example is one that creates a drum pattern with an instrument of a drum set. By creating multiple patterns and combining them into one, you can program the drum section of a song.

kick = note('16c2’)
clap = note('16f2’)
drum = merge(
notemap('!...!...!...!', kick)
notemap('.!!..!!..!!..', clap))

This script starts by defining two variables kick and clap with note objects. The duration is 1/16. Next the drum variable is the result of merging 2 patterns created with a notemap. Using “dots and bangs”, you can notate the occurrence of a note inside the pattern. If an exclamation mark (!) is seen then the kick will be played. Dots are needed to get the timing right. You can merge any number of patterns with any number of notes.

Play

An important step in the creative process of making music is to get fast audible feedback. Starting from an initial idea, you want to try out variations and combinations by repeatedly making a change and listen to the result. To fully support this process, a program (the melrose tool) is needed that is able to understand the language and can produce the sounds from the written expressions.

Many programs exist that can produce sounds and are known as digital audio workstations (DAW) e.g Ableton, LMMS and Logic. The standard way to communicate with these tools is by sending MIDI messages. Playing one note requires two MIDI messages ; one to start the note and one to stop the note at a later moment in time.

So for the melrose tool to produce sound from a musical object, it just needs to translate the object into sequences of notes and send MIDI messages with the right timing, duration and tempo (bpm).

Loops for live feedback

A powerful feature of the melrose tool is the ability to play musical objects in a timing loop while modifying any object that is part of it. To understand this, let us have a look at a small example:

p = 0
loop( transpose (p, sequence('c = e = g =')))

If you play this program (an assignment and a loop) then you will hear the 3 notes C,E,G separated by a rest, over and over again.

For most programming languages, a program is first compiled into an artifact that then can be run. However, this means that any change to the program would require stopping it, changing the program, compile and run it again. This would also mean that all sounds will silenced which clearly negatively impacts the flow of creativity. Therefore instead, the melrose tool is not compiling the program but evaluating the expressions in the program on each loop entry.

So while running the loop from the example above, you can change the value of p to a different number which will cause the sequence to be transposed with a new number of semitones. This applies to many other kinds of changes, even the loop itself can be changed while it is running.

Parallel

Melrose can play any number of musical objects at the same time because it uses its own clock to produce notes in the specify tempo (bpm). For example, you can start playing a loop and while listening to it, you can program another and play that one and so on. If “m” is a melody and “b” is the bass line and “d” is the drum layer then you can either play:

sync( m, b, d)  // play all at once

or start and stop loops with your own timing. Any next loop will start and the end of the first loop. So if you play loop d first and later want to start playing loop b, it will actually start on the next loop start of d.

loop(d) // if you play this first
loop(b) // then playing this later will sync with the next d
loop(m) 

Device and channel

Music is typically arranged with multiple instruments playing together melodies, rythmic drums or others sounds. Because melrose is a MIDI producing tool, it can leverage the available MIDI devices and use up to 16 MIDI channels per device.

Without specifying a device or channel, the default values are used to play a musical object. To send the notes to one specific device and channel, you can use enclosing functions to make that happen.

c = chordprogression('c', 'i iv v')
loop( device(2, channel(4, c))

The device id “2” refers to an entry in your MIDI devices list.

Key trigger

An alternative way to start playing (and stopping) melodies using the editor and keyboard shortcuts, is to use a MIDI hardware device to trigger an action. Using the functions key and listen you can program what should happen if you hit a key on your controller or turn a knob.

onkey('c',loop1) // if the C is played on the default input MIDI device then start or stop loop1

Final note :-)

Any programming language should have just enough features to allow you to create as much as possible. The language will only be extended with more functions if that new feature cannot be easily achieved by composition of existing functions.

The tool Melrōse is not and will not become a DAW. Instead it can feed into it because any DAW can be a MIDI output device from the perspective of the Melrōse tool [2].

The features of the tool and its plugin is driven by its actual use by the author and hopefully other interested musicians/programmers. If you want to contribute somehow, please go the project site [3].

[1] Algorithmic_composition

[2] melrōse.org

[3] sources on github