Music data extraction

The functions described in this page allow for easier extraction of data from music recordings (in the form of MIDI). A highlight of JuliaMusic is the timeseries function, which allows one to directly get gridded timeseries from arbitrary Notes structures.

Basic Data Extraction

MusicManipulations.firstnotesFunction
firstnotes(notes, grid)

Return the notes that first appear in each grid point, without quantizing them.

This function does not consider the notes modulo the quarter note! Different quarter notes have different grid points.

MusicManipulations.filterpitchesFunction
filterpitches(notes::Notes, filters) -> newnotes

Only keep the notes that have a pitch specified in filters (one or many pitches).

MusicManipulations.separatepitchesFunction
separatepitches(notes::Notes [, allowed])

Get a dictionary "pitch"=>"notes of that pitch". Optionally only keep pitches that are contained in allowed.

MusicManipulations.combineFunction
combine(note_container) -> notes

Combine the given container (either Array{Notes} or Dict{Any, Notes}) into a single Notes instance. In the process, sort the notes by position in the final container.

MusicManipulations.relposFunction
relpos(notes::Notes, grid)

Return the relative positions of the notes with respect to the current grid, i.e. all notes are brought within one quarter note.

Advanced Data Extraction

MusicManipulations.estimate_delayFunction
estimate_delay(notes, grid)

Estimate the average temporal deviation of the given notes from the quarter note grid point. The notes are classified according to the grid and only notes in the first and last grid bins are used. Their position is subtracted from the nearby quarter note and the returned value is the average of this operation.

MusicManipulations.estimate_delay_recursiveFunction
estimate_delay_recursive(notes, grid, m)

Do the same as estimate_delay but for m times, while in each step shifting the notes by the previously found delay. This improves the accuracy of the algorithm, because the distribution of the quarter notes is estimated better and better each time. The function should typically converge after a couple of m.

The returned result is the estimated delay, in integer (ticks), as only integers can be used to actually shift the notes around.

Timeseries

MusicManipulations.timeseriesFunction
timeseries(notes::Notes, property::Symbol, f, grid; kwargs...) -> tvec, ts

Produce a timeseries of the property of the given notes, by first quantizing on the given grid (to avoid actual quantization use the grid 0:1//notes.tpq:1). Return the time vector tvec in ticks and the produced timeseries ts.

After quantization, it is often the case that many notes are in the same bin of the grid. The function f denotes which value of the vector of the property of the notes to keep. Typical values are minimum, maximum, mean, etc. Notice that bins without any note in them obtain the value of the keyword missingval, which be default is just missing, regardless of the function f or the property.

If the property is :velocity, :pitch, or :duration the function behaves exactly as described. The property can also be :position. In this case, the timeseries ts contain the timing deviations of the notes with respect to the tvec vector (these numbers are known as microtiming deviations in the literature).

If given keyword 'segmented = true', the notes are segmented according to the grid in order to respect the information of their duration, see segment. Otherwise the notes are treated as point events with no duration (it makes no sense to choose :duration with segmented).

timeseries(notes::Notes, f, grid) -> tvec, ts

If property is not given, then f should take as input a Notes instance and output a numeric value. This is useful for example in cases where one would want the timeseries of the velocities of the notes of the highest pitch.


Here is an example:

using MusicManipulations, PyPlot, Statistics
midi = readMIDIFile(testmidi())
notes = getnotes(midi, 4)

swung_8s = [0, 2//3, 1]
t, vel = timeseries(notes, :velocity, mean, swung_8s)

notmiss = findall(!ismissing, vel)

fig, (ax1, ax2) = subplots(2,1)
ax1.scatter(t[notmiss], vel[notmiss])
ax1.set_ylabel("velocity")

t, mtd = timeseries(notes, :position, mean, swung_8s)
ax2.scatter(t[notmiss], mtd[notmiss], color = "C1")
ax2.set_ylabel("timing deviations")
ax2.set_xlabel("ticks")


An example code that obtains the velocity of the notes with highest pitch in each bin is this one:

notes = getnotes(midi, 4)

function f(notes)
    m, i = findmax(pitches(notes))
    notes[i].velocity
end

grid = 0:1//3:1
tvec2, ts2 = timeseries(notes, f, grid)

Segmentation

MusicManipulations.segmentFunction
segment(notes, grid) → segmented_notes

Quantize the positions and durations of notes and then segment them (i.e. cut them into pieces) according to the duration of a grid unit. This function only works with AbstractRange grids, i.e. equi-spaced grids like 0:1//3:1.