Tuesday 12 April 2011

Quantising monophonic MIDI melodies

Quantisation is a useful technique in its own right, but I personally
tend to use it more often as a pre-processing tool. By applying quantisation
we can enforce a minimum granularity, which helps keep a large dataset
of melodies (such as the Essen dataset) consistent.

Quantisation should really be simple as pie - just round the note start
times to a minimum subdivision. However, monophony complicates
things slightly. If two notes get rounded to the same start time,
which one do you chose? Where does the other one go? In the
implementation below I keep the note that has an original start time
closest to the quantised start time. Any colliding notes get
discarded.

Another question is whether the notes' durations should be
quantised or if they should keep their original durations. If the
original duration is maintained, one is quite likely to end up with
overlapping notes, but if durations are quantised, one could lose
rests. Since requirements might vary, I leave the choice to the user.

Finally, since many melodies begin with an upbeat, we need a way to
offset quantisation. This is particularly useful when dealing with
quite aggressive quantisation (e.g. quantisation to nearest crotchet).

Enough talk, here's the code:

# notes is the input matrix,
# subdiv the minimum subdivision (in crotchets),
# offset the quantisation offset (also in crotchets).
# preserve.duration determines whether to tie notes or
# preserve original durations.
quantise.notes <- function(notes, subdiv, offset = 0,
preserve.duration = TRUE)
{
if(subdiv == 0)
return(notes)

# first, do "naive" quantising, by just rounding start times
subdiv.ticks <- subdiv * midi.get.ppq()
offset.ticks <- offset * midi.get.ppq()
notes.quantised <- notes
notes.quantised[, "start"] <-
round((notes.quantised[, "start"] - offset.ticks) /
subdiv.ticks) * subdiv.ticks + offset.ticks

# find any collisions
tbl <- sort(table(notes.quantised[, "start"]))
collisions <- as.numeric(names(tbl[tbl > 1]))

# if there are collisions, remove all notes that are not
# the note closest to the quantised value
if(length(collisions) > 0) {
remove.indices <- integer(0)
for(collision in collisions) {
colliding.indices <- which(notes.quantised[, "start"] ==
collision)
distances <- abs(notes[, "start"] - collision)

# if two colliding notes are equally close,
# use the lower one
closest.index <- which(distances == min(distances))[1]

remove.indices <- c(remove.indices,
colliding.indices[colliding.indices !=
closest.index])
}

# remove colliding notes
notes.quantised <- notes.quantised[-remove.indices, ]
}

# if we don't preserve the original durations, we tie the notes
if(!preserve.duration) {

# order notes by start time
notes.quantised <-
notes.quantised[order(notes.quantised[, "start"]), ]

# tie all notes except last note
for(i in 1:(nrow(notes.quantised) - 1)) {
notes.quantised[i, "duration"] <-
notes.quantised[i + 1, "start"] -
notes.quantised[i, "start"]
}

# set the duration of the last note such that the duration of
# the quantised melody is the same as the duration of the
# original melody
notes.quantised[nrow(notes.quantised), "duration"] <-
notes[nrow(notes), "duration"] +
notes[nrow(notes), "start"] -
notes.quantised[nrow(notes.quantised), "start"]
}

return(notes.quantised)
}

The input to this function is a matrix with the column headers
"start", "duration", "pitch" and "velocity". In the file hosted on
Github
I provide functions from converting to this format from RMidi
matrices
, and back again.

No comments:

Post a Comment