Design Considerations¶
This document is an early design document, probably supplanted by other documentation, but reviewed and converted to Markdown on 10 Jan 2026, so it should be accurate.
Terminology and Structure¶
-
onsetrefers to the start time of a note or other event. -
offsetrefers to the ending time of a note or other event. -
durationrefers tooffset - onset -
Scores are hierarchical, but all nodes and leaves represent
onsetdirectly in absolute beat or time (not relative to the onset time of the parent). The end time is calledoffsetand is computed as onset + duration. (Note that in contexts outside of AMADS, "offset" is a synonym for "displacement," which is essentially a "delta" or "difference," so be careful not to be confused: "onset" is the beginning, "offset" is the ending!)
The use of absolute onset times makes it relatively expensive to shift
an object in time. E.g. to move a measure to a later time, you must
shift not only the onset of the Measure but also all of the
Notes and other objects in the Measure. However, the most
common reason to use a Measure or some other object at another
time is to reuse the object in a different context, e.g. to copy a
measure from one part to another part. Since all member objects have a
reference to their parent, the copy must be a deep copy of the entire
subtree. Given the cost of the deep copy, the cost of recomputing the
onsets is negligible. In addition, using an absolute onset
simplifies a lot of code and makes access to the onset more
efficient.
The music representation hierarchy is constrained to match intuitive
ideas about music structure. The levels of hierarchy are Score,
Part, Staff, Measure, Chord, Note, and
Rest. Often, this is overkill for the task at hand, so we support
a "flat" representation which is basically a list of Note
objects. More on this in the next section.
The edge or leaf-nodes of the representation hierarchy include
Note and Rest classes. All of these inherit from an abstract
superclass called Event, which is basically just onset,
duration and parent attributes. The Note subclass adds
attributes for pitch, dynamic, tie, etc.
Inner levels of hierarchy are represented by subclasses of
EventGroup, which is in turn an Event with an added attribute
named content, a list of children. E.g. a Measure is an
EventGroup. You might expect the duration of an EventGroup to
be implied by the content, but especially if Rests are removed, a
Measure has a duration that is independent of the content. An
EventGroup duration is mainly relevant of you want to append nodes
sequentially: The duration gives you the offset time, which becomes
the onset time of the next appended element.
Pitch is structured and includes a representation of accidentals, so AMADS can disambiguate between Bb and A#, for example.
Time¶
Time is normally measured in quarters, regardless of the meter. So a measure in 4/16 time has a duration of 1 (not 4). Scores have a tempo and information corresponding to a Standard MIDI File tempo track, so AMADS can convert from quarters to seconds or seconds to quarters.
To work in seconds, it is most efficient to convert the entire score
to seconds, meaning that every onset and duration attribute is
converted in one pass through the score. Since the entire score stores
time in one unit or the other, there are no separate attributes such as
offset_seconds or duration_seconds, and computed values such as
offset run identical computations whether the units are beats or
seconds, so they return values in the same units as are stored in the
score.
Conversion to Seconds¶
Conversion to seconds is done in-place. This violates the immutable
Score design (see next section) but note that if you convert back to
beats, you can restore the original score, so changes are not
necessarily visible even when scores are shared. Conversion involves
using the score TimeMap to map between quarters and seconds. The
TimeMap is stored as an array of breakpoints, so a lookup involves
a search, but since most accesses are in time order, it is possible to
cache the location of the previous lookup in case the next one is
nearby. Thus, we can expect to make a single pass through the
TimeMap to convert each Staff and its children.
Immutable Scores¶
In general, music structures in AMADS are immutable. This means that once you read or construct a score, you can extract data and reformat the data without side-effects from analysis functions. For example, given a piano score, you can extract the top staff into a new score. This will not delete the bottom staff, so you can still extract the bottom staff into another score. Similarly, if you merge all tied notes in a score, any references you had to notes before the merge will remain unaffected because the notes will be merged in a copy.
Elements of the Score hierarchy have access to their parent, so parents cannot generally share sub-trees. E.g. to extract the flute Part from a complete Score, AMADS cannot simply construct a new Score that contains just the flute Part, because the original flute Part's parent is the original Score. Instead, the entire flute Part (but no other Parts) must be deep-copied to assign new parents to elements of the copy.
It would be prohibitively expensive to rule out all modifications to
Scores, e.g. transposing 100 notes should not make 100 copies of the
original score, making one change at a time. The rule is that once you
copy a score, you can make any number of modifications as long as
there are no shared references to the score or score elements. Thus,
scores are not observably mutable from the "outside" of
computations. AMADS transformations such as flatten or
merge_tied_notes generally make a single copy even though there
are many changes.
Another case is that some analysis algorithms need to report results
on a per-note basis, e.g. assigning an entropy value to each note. In
these cases, the analysis algorithm can create and set an
analysis-specific attribute, e.g. entropy, on each
note. Essentially, algorithms are allowed to annotate scores by
adding new events and attributes, as long as they do not change
existing attributes or insert or delete Notes, Rests, Chords or other
event types understood by AMADS.
In keeping with Python best practices, we do not use Python's
ability to add attributes to objects dynamically. Instead, there
is one attribute, info, on every Event that serves as a dictionary
for additional information that does not have a place in the
declared attributes. We consider info to be mutable, so analysis
algorithms and their users should be careful to avoid overwriting
information there.
Basic Representation and Simplifications¶
The “standard” score has parts representing instrumental parts. Each part has 1 or more staves (e.g. a piano part would ordinarily have 2 staves [“staves” is the plural of “staff”, but documentation often uses “staffs” in the sense of “multiple instances of the Staff class.”]), each staff has measures, and measures have notes and chords which have notes. Notes can be tied, so a single “logical” note can be represented (as in standard notation) by multiple notes tied together. This is basically because notes do not fit within the hierarchy imposed by measures and beats. When a note crosses a measure boundary, and sometimes when it crosses a beat boundary, at least in conventional notation, it must be split across these boundaries and the original note is indicated by ties between the fragments.
All this hierarchy can get in the way, so we allow various simplifications:
-
Tied notes can be replaced by single notes, with durations that may extend beyond a measure boundary (
merge_tied_notes()method) -
Rests can be removed (
remove_rests()method) -
Staves within each part can be collapsed to a single staff (currently, no method does just this)
-
Chords can be removed, moving notes “up” into measures (
expand_chords()method) -
Staves, measures and chords can be removed and all "leaf" notes moved directly into parts (
flatten()method, which additionally removes ties as inmerge_tied_notes()since there are no more explicit measures) -
Multiple parts can be combined into a single part (
merge_part()method, but this also doesflatten()which impliesmerge_tied_notes())
So there are lots of variations all having to do with removing different hierarchies. We considered a hierarchy of representations, each with additional notation details or hierarchy, but this seems too complicated and while it might be appropriate for certain kinds of analysis, the intermediate levels of simplification do not correspond to any familiar notation and so they are not intuitive.
In conclusion, the main thing users should think about is measure structure vs. “flat” note lists, so we have two categories for scores: full and flat. Within these types, we can have checks for the more subtle differences and operations to remove structure:
Full Scores¶
.has_rests()
The Score or Part or Staff or Measure has one or more Rest objects.
.remove_rests()
Construct a Score or Part or Staff or Measure without any
Rests. Note that removing rests does not change the timing of
notes or other objects since each Events has a delta time relative
to the parent (as opposed to music notation where a note begins
after a previous note or rest).
.has_chords()
The Score or Part or Staff or Measure has one or more Chord objects.
.expand_chords()
Convert a Score, Part, Staff or Measure to one without chords
(chord notes become ordinary notes within the parent).
.has_ties()
The Score, Part, Staff or Measure has one or more tied notes.
.merge_tied_notes()
Convert the Score, Part, Staff or Measure to one without
ties. Although not required, we expect ties to break notes where
they cross measure boundaries. After .merge_tied_notes(),
notes may cross one or more measure boundaries.
.remove_measures()
The .remove_measures() method "lifts" notes into the Staff
level, preserving each Staff. This is neither a Full Score
nor a Flat Score, but might be useful in processing each Staff
separately. Note that tied notes can cross staves.
remove_measures() merges ties to eliminate staff-crossings.
For example if (staff 1, note 1) ties to (staff 2, note 2), then
note 2 will be removed from staff 2 (and the duration of note 1
will be adjusted).
.flatten()
Convert a full score into a flat score. Parts are preserved or
collapsed based on an optional parameter. Tied notes are always
merged because we assume ties are not useful in this
context. Non-Note events are not retained in Part(s) because they
might only be relevant within the hierarchy of a measured
score. However, non-Note events can be inserted into a flat score.
.collapse_parts()
Merge the notes of selected Parts and Staffs into a flat score
with one Part. When called with no part or staff selection, all
notes are combined this is equivalent to
.flatten(collapse=True).
Flat Scores¶
.is_flat()
Test if this is a flat score. A flat score has a strict hierarchy
described by: Score-Part-Note. There are no tied notes. Also,
there are no Staff, Measure, Rest or Chord objects, but there may
be other subclasses of Events at any level.
.is_flat_and_collapsed()
Test if this is a flat score with one and only one Part.
.part_count()
Returns number of parts
Other Scores¶
Scores which are neither Full nor Flat are at least possible to construct. E.g. a Score-Part-Note hierarchy with tied notes or a Score with a mix of measured and flattened Parts. Developers should consider that valid Full Scores could have Chord objects with zero or one Notes.
Ideally, algorithms should detect violations in assumptions and report them as errors: We do not want users to call functions with an intuitive idea of what they should do, only to get some non-intuitive result that the user does not notice. It's better to raise an error to say “you can't do this, or I don't support it” than to silently return something possibly wrong.
The .is_well_formed() and .is_well_formed_full_score() methods
can check for conformance to the standard forms.
Distributions and Histograms¶
The Distribution class models statistical distributions or
histograms. Attributes describe the data with enough detail to produce
reasonably labeled plots, so by returning a Distribution object
rather than simply a vector, the user can call .plot() without
even knowing the proper axes and labels.
Distributions are often built with a Histogram object to do calculations, but the Histogram class can be used independently of Distributions, and neither is a subclass of the other.