MNS: a formalism for musical nuance
|
random trip report |
David P. Anderson
Score documentsWe also take the approach of separating document and annotation. In our case, a "score document" describes a set of notes. This could have the form of a MusicXML file (which can be generated by most score editors) or a Music21 object hierarchy, or a MIDI file. The attributes of a note include:
In addition, the score document can specify a set of "measures". Each is described by its start time, its duration, and a "type" tag, a string representing its duration and structure (e.g. "2+2+3/8"). If measures are specified, each note N has two additional attributes:
Note selectorsMNS provides a powerful way of identifying sets of notes within a score document. A "note selector" is a Boolean expression involving the attributes of a note N. We use Python syntax for these expressions. For example,
'rh' in N.tags and N.dur == 1/2 selects all half notes in the right hand. We could select notes in a particular time range, at a particular measure offset, and so on. Note: CSS also has expressions. Piecewise functions of timeIn MNS, changes in tempo and dynamics can be described as functions of (score) time. These functions are specified as a sequence of "primitives", each of which represents a parameterized function defined over a time interval with a given duration. A function defined in this way is called a "piecewise function of time" (PFT). For example, linear(80, 50, 2/1), linear(50, 90, 4/1) defines a tempo function that slows linearly from 80 BPM to 50 BPM over 2 measures, then speeds up to 90 BPM over 4 measures. There are many possible PFT primitives: polynomial, exponential, trigonometric, etc. Research into nuance in actual performance will hopefully shed light on what primitive types are most useful. In MNS, PFTs are used for two purposes:
For tempo PFTs, an additional primitive is available:
delta(dt, after=True) This inserts a pause of dt seconds. If after is True, the pause occurs after notes at the current score time.
Dynamic controlNote volume is represented as 0..1. Notes initially have volume 0.5. Volume adjustments are multiplicative factors. The primitive
vol_adjust_pft(t0, pft, selector=None) adjusts the volume of a set of notes according to a function of time. "pft" is a PFT, and "selector" is a note selector The volume of a selected note N in the domain of the PFT is adjusted by the factor pft(t), where t is N.time - t0. This can be used to set the overall volume of the piece. It can be used to shape the dynamics of an inner voice by selecting the tag used for that voice. Other primitives adjust volume explicitly (not necessarily as a function of time). vol_adjust(factor, selector=None) vol_adjust(func, selector=None)These adjust the volumes of the selected notes. If the 1st argument is a function, its argument is a note and it returns an adjustment factor. Otherwise the 1st argument is an adjustment factor. For example,
vol_adjust(lambda n: random.normal()*.01) makes a small normally-distributed adjustment to the volume of all notes.
vol_adjust(ns, .9, lambda n: n.measure_offset == 2) vol_adjust(ns, .8, lambda n: n.measure_offset in [1,3]) vol_adjust(ns, .7, lambda n: n.measure_offset not in [0,1,2,3]) emphasizes the strong beats of 4/4 measures. Timing controlThe timing of a note (its start time and duration) is described in both "score time" (in which the unit is a 4/4 measures) and "performance time" (in which the unit is one second). Initially, performance time is score time times 4, so that 1 beat (i.e. a quarter note) equals 1 second. The score time of notes is fixed. Timing primitives manipulate their performance time.
tempo_adjust_pft(pft, selector=None, normalize=False, bpm=True) This adjusts the performance time of the selected notes according to a function F specified by pft. If bpm is False, the value of F is the rate of change of performance time with respect to score time. The performance duration of a score-time interval is the integral of F over that interval. We call this an "inverse tempo function" because larger values mean slower: 2.0 means go half as fast, 0.5 means go twice as fast. If "bpm" is True, the value of F is in beats per minute. For example, 120 means go twice as fast. F represents tempo rather than inverse tempo. If "normalize" is set, F is scaled so that its average value is one. This can be used, for example, to apply rubato a particular voice over a given period, and have it synch up with other voices at the end of that period. The semantics of this function:
pause_before(t, dt) Add a pause of dt seconds before score time t. Earlier notes that end at or after t are elongated.
pause_after(t, dt) Add a pause of dt seconds after score time t. Notes that start at t are elongated.
roll(t, offsets, is_up=True, is_delay=False) Roll a chord. "offsets" is a list of time offsets (typically negative). These offsets are added to the performance start times of notes that start at score time t. If "is_up" is true, they are applied from bottom pitch upwards; otherwise from top pitch downward. If "is_delay" is True, the range of the offsets is added to subsequent notes, so that the roll adds a delay.
t_adjust_list(ns, offsets, selector) "offsets" is a list of time offsets (seconds). They are added to the start times of notes satisfying the selector, in time order.
t_adjust_notes(ns, offset, selector) The given time offset (seconds) is added to the start times of all notes satisfying the selector.
t_adjust_func(ns, func, selector): For each note satisfying the selector, the given function is called with that note, and the result is added to the note's start time.
Articulation control
perf_dur_rel(factor, pred) Multiply the duration of the selected notes by the given factor.
perf_dur_abs(t, pred) Set the duration of the selected notes to the given value (seconds).
perf_dur_func(f, pred) Set the duration of a selected note N to the value f(N). Pedal controlAn application of a pedal is represented by
pedal(start, end, type) where "start" and "end" are in score time, and "type" is sustain, soft, or sostenuto. Timing control primitives affect pedal events as well as notes. LayeringFor the most part, MNS primitives don't set note parameters; rather they adjust the parameters. This means that nuances can be "layered" as mentioned earlier. We could imagine having several MNS specifications, each in a separate file, applied in sequence to a score, analogously to CSS stylesheets. Extending and refining MNSMNS as described here is version 0. It's a good framework, but it's fairly crude and low-level. I find that each new piece I work on requires new MNS features. There's lots of room for improvement. One area of work is PFT primitives. Linear primitives - all I've implemented so far - are not quite convincing for either tempo or volume control. Another important area of work, I think, is the way notes are selected. What we have now is basic: notes are tagged based on chord position and metric position, you can tag them explicitly. I can imagine more sophisticated ways of selecting notes, based on musical semantics:
It might be useful to parameterize MNS primitives, so that you can adjust the overall level of nuance by turning a single "knob". Representing MNS specificationsThe primitives described here have been implemented in a Python library called Numula. For interoperability with other musicc software, we need a file format for MNS specifications. I lean toward JSON as a basis for this; one could also use XML, but it's unnecessarily verbose. An MNS specification in JSON might look like: { "title": "MNS spec for Chopin's 1st Nocturne", "operators": [ { "type": "tempo_adjust_pft", "start": "3/4", "segments": [ { "type": "linear", "start": 90, "end": 110, "dur": "8/4", }, { "type": "delta", "value": .15 }, { "type": "linear", "start": 110, "end": 80, "dur": "7/4" } ], "normalize": "True", "selector": "'rh' in tags" }, { "type": "vol_adjust_pft", "start": 0, "segments": [ { "type": "linear", "start": .3, "end": .8, "dur": "2/4", "closed_end": "True" }, { "type": "linear", "start": .7, "end": .3, "dur": "1/4", } ] } ] } |