Meter
attractor_tempos
¶
What does it mean for music to be "fast" or "slow"? Certainly BPM is not enough. The "Attractor tempos" theory (Gotham 2015, [1]) proposes a definition of "fast"/"slow" relative to neutral, central, moderate tempos and a definition of those moderate ("Attractor") tempos that accounts for the role of the metrical structure. In short, it provides a model for optimizing the salience of metrical structures.
Author: Mark Gotham
Reference
[1] Gotham, M. (2015). Attractor tempos for metrical structures. Journal of Mathematics and Music, 9(1), 23–44. https://doi.org/10.1080/17459737.2014.980343
Classes¶
MetricalSalience
¶
MetricalSalience(
symbolic_pulses: Optional[ndarray] = None,
quarter_bpm: Optional[float] = None,
mu: float = 0.6,
sig: float = 0.3,
)
Methods for storing array representations of metrical structure and derived salience values.
Parameters:
-
symbolic_pulses(Optional[ndarray], default:None) –A NumPy array representing the symbolic pulse lengths by level.
-
quarter_bpm(Optional[float], default:None) –The beats-per-minute corresponding to the symbolic value of a pulse length 1.0 in symbolic time. The user sets this value if/when calculating absolute length and salience values.
-
mu(float, default:0.6) –The mean of the Gaussian.
-
sig(float, default:0.3) –The standard deviation of the Gaussian.
Attributes:
-
symbolic_pulses–As above.
-
absolute_pulses–An adaptation of the symbolic pulse lengths array that maps each value from symbolic to seconds.
-
salience_values–An adaptation of the absolute pulse lengths to the equivalent salience values (see notes on
log_gaussian). -
cumulative_salience_values–A 1D array summation of the absolute salience values by column (one value per metrical position).
-
indicator–An indicator array for the (non-)presence of values at each position of the symbolic pulse lengths array. This can serve, for example, as the symbolic equivalent of the (absolute)
salience_valuesarray.
Examples:
>>> from amads.time.meter.representations import PulseLengths
>>> pl = [4, 2, 1, 0.5]
>>> pls = PulseLengths(pulse_lengths=pl, cycle_length=4)
>>> arr = pls.to_array()
>>> arr
array([[4. , 0. , 0. , 0. , 0. , 0. , 0. , 0. ],
[2. , 0. , 0. , 0. , 2. , 0. , 0. , 0. ],
[1. , 0. , 1. , 0. , 1. , 0. , 1. , 0. ],
[0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5]])
>>> ms = MetricalSalience(symbolic_pulses=arr, quarter_bpm=120)
>>> ms.absolute_pulses
array([[2. , 0. , 0. , 0. , 0. , 0. , 0. , 0. ],
[1. , 0. , 0. , 0. , 1. , 0. , 0. , 0. ],
[0.5 , 0. , 0.5 , 0. , 0.5 , 0. , 0.5 , 0. ],
[0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25]])
>>> ms.cumulative_salience_values
array([2.39342011, 0.44793176, 1.4136999 , 0.44793176, 2.17446773,
0.44793176, 1.4136999 , 0.44793176])
Source code in amads/time/meter/attractor_tempos.py
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 | |
Functions¶
calculate_absolute_pulse_lengths
¶
calculate_absolute_pulse_lengths()
Calculate absolute pulse lengths from
the symbolic lengths (symbolic_pulses) and
the BPM provided here for the 'quarter note' as reference value.
Source code in amads/time/meter/attractor_tempos.py
102 103 104 105 106 107 108 | |
calculate_salience_values
¶
calculate_salience_values()
Calculate salience values for items in the symbolic_pulses
using log_gaussian (see notes on that function).
Source code in amads/time/meter/attractor_tempos.py
110 111 112 113 114 115 | |
calculate_cumulative_salience_values
¶
calculate_cumulative_salience_values()
Calculate cumulative salience values by summing over columns.
Source code in amads/time/meter/attractor_tempos.py
117 118 119 120 121 | |
make_indicator
¶
make_indicator()
Make a 2D indicator vector for the presence/absense of a pulse value at each position.
Source code in amads/time/meter/attractor_tempos.py
123 124 125 126 127 | |
plot
¶
plot(
symbolic_not_absolute: bool = False,
reverse_to_plot: bool = True,
show: bool = True,
)
Plot the salience values with their respective contribution.
Parameters:
-
symbolic_not_absolute(bool, default:False) –If False (default), plot the tempo- and meter-sensitive, weighted salience values.
-
reverse_to_plot(bool, default:True) –
Returns:
-
Figure–A matplotlib.figure.Figure of the plotted salience values.
Source code in amads/time/meter/attractor_tempos.py
129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 | |
Functions¶
log_gaussian
¶
log_gaussian(arr: ndarray, mu: float = 0.6, sig: float = 0.3)
Compute a log-linear Gaussian which is the basis of individual pulse salience values.
To avoid log(0) issues, np.clip values to be always greater than 0.
See also MetricalSalience.calculate_salience_values.
Parameters:
-
mu(float, default:0.6) –The mean of the Gaussian.
-
sig(float, default:0.3) –The standard deviation of the Gaussian.
Examples:
>>> log_gaussian(np.array([0.06, 0.6, 6.0])) # demo log-lin symmetry
array([0.00386592, 1. , 0.00386592])
>>> log_gaussian(np.array([0.5, 1., 2.])) # 2x between levels
array([0.96576814, 0.76076784, 0.21895238])
Source code in amads/time/meter/attractor_tempos.py
183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 | |
break_it_up
¶
This module serves to map out metrical hierarchies in a number of different ways and to express the relationship between the notes in and the hierarchy of a metrical cycle.
Uses include identifying notes that traverse metrical levels, for analysis (e.g., as a measure of syncopation) and notation (e.g., re-notating to reflect the within-measure notational conventions).
Author: Mark Gotham
Classes¶
MetricalSplitter
¶
MetricalSplitter(
note_start: float,
note_length: float,
start_hierarchy: list[list],
split_same_level: bool = True,
)
Split up notes and/or rests to reflect a specified metrical hierarchy.
This class takes in a representation of a note in terms of the start position and duration, along with a metrical context and returns a list of start-duration pairs for the constituent parts of the broken-up note.
The metrical context should be expressed in the form of a start_hierarchy
(a list of lists representing the hierarchy from the coarsest level to the finest).
This can be provided directly or made via various classes in the meter module (see notes there).
The basic premise here is that a single note can only traverse metrical boundaries for levels finer than the one it starts on. If it traverses the metrical boundary of a coarser level, then it is split at that position into two note-heads. This split registers as a case of syncopation for those algorithms and as a case for two note-heads to be connected by a tie in notation.
There are many variants on this basic setup. This class aims to support almost any such variant, while providing easy defaults for simple, standard practice.
The flexibility comes from the definition of a metrical structure
(for which see the MetricalHierarchy class).
Each split of the note duration moves up one metrical level.
The exact definition of metrical structure
(including the presence and absence of each level) is central.
Given a "4/4" that's defined by pulse levels 4, 2, 1, 0.5, 0.25
(PulseLengths([4, 2, 1, 0.5, 0.25], cycle_length=4)),
a note of duration 2.0 starting at
0.25 connects to 0.5 in level 3 (duration = 0.25), then
0.5 connects to 1.0 in level 2 (duration = 0.5), then
1.0 connects to 2.0 in level 1 (duration = 1.0), and
this leaves a duration of 0.25 to start on 2.0.
The data is returned as a list of (position, duration) tuples.
The values for the example would be:
[(0.25, 0.25),
(0.5, 0.5),
(1.0, 1.0),
(2.0, 0.25)]
Alternatively, the metrical encoding of 4/4 might decline to specify
the levels for pulse length 2 and any shorter than 1.
This is true of default construction with TimeSignature(as_string="4/4").
Clearly this yields different splits.
Both of these variants are demonstrated among the examples below.
If the note runs past the end of the metrical span,
the remaining value is stored with the
start_duration_pairs recording the within-measure pairs
and remaining_length attribute for the rest.
If the note_start is not in the hierarchy,
then the first step is to map to the "next" (larger) value in the finest level specified.
Parameters:
-
note_start(float) –The starting position of the note (or rest).
-
note_length(float) –The length (duration) of the note (or rest).
-
start_hierarchy(list[list]) –Metrical hierarchy from the coarsest level to finest. Each level must be a superset of the one above it. Level 0 must be
[0.0, cycle_length]. -
split_same_level(bool, default:True) –If True, split at boundary positions within the level the note starts on, in addition to boundaries between levels. Relevant for meters with 3-groupings such as 6/8: a quarter note starting on the second eighth note (position 0.5) can either be left intact or split on the third eighth (position 1.0) depending on this editorial convention. Defaults to
True.
See Also
nested_to_start_hierarchy : Convert a variably-nested list of positions
into a start_hierarchy suitable for this class.
Examples:
>>> from amads.time.meter.representations import TimeSignature, PulseLengths
>>> m = TimeSignature(as_string="4/4")
>>> start_hierarchy = m.to_start_hierarchy()
>>> start_hierarchy
[[0.0, 4.0], [0.0, 1.0, 2.0, 3.0, 4.0]]
This shows that the TimeSignature(as_string="4/4")
specifies the /1 and /4 levels only (skipping the /2 level and any shorter than the /4).
Here, in the absence of the /2 level, the split_same_level option makes a difference.
>>> split = MetricalSplitter(0.25, 2.0, start_hierarchy=start_hierarchy, split_same_level=False)
>>> split.start_duration_pairs
[(0.25, 0.75), (1.0, 1.25)]
>>> split = MetricalSplitter(0.25, 2.0, start_hierarchy=start_hierarchy, split_same_level=True)
>>> split.start_duration_pairs
[(0.25, 0.75), (1.0, 1.0), (2.0, 0.25)]
>>> m.fill_2s_3s()
>>> start_hierarchy = m.to_start_hierarchy()
>>> start_hierarchy
[[0.0, 4.0], [0.0, 2.0, 4.0], [0.0, 1.0, 2.0, 3.0, 4.0]]
Now we have the /2 level, the split_same_level option makes no difference.
>>> split = MetricalSplitter(0.25, 2.0, start_hierarchy=start_hierarchy, split_same_level=True)
>>> split.start_duration_pairs
[(0.25, 0.75), (1.0, 1.0), (2.0, 0.25)]
>>> split = MetricalSplitter(0.25, 2.0, start_hierarchy=start_hierarchy, split_same_level=False)
>>> split.start_duration_pairs
[(0.25, 0.75), (1.0, 1.0), (2.0, 0.25)]
Creating directly from PulseLengths is clearer in this case:
>>> meter_from_pulses = PulseLengths([4, 2, 1, 0.5, 0.25], cycle_length=4)
>>> start_hierarchy = meter_from_pulses.to_start_hierarchy()
>>> start_hierarchy[-1]
[0.0, 0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 3.25, 3.5, 3.75, 4.0]
>>> split = MetricalSplitter(0.25, 2.0, start_hierarchy=start_hierarchy)
>>> split.start_duration_pairs
[(0.25, 0.25), (0.5, 0.5), (1.0, 1.0), (2.0, 0.25)]
Now with a note that extends beyond the end of the cycle:
>>> split = MetricalSplitter(0.25, 4.0, start_hierarchy=start_hierarchy)
>>> split.start_duration_pairs
[(0.25, 0.25), (0.5, 0.5), (1.0, 1.0), (2.0, 2.0)]
>>> split.remaining_length
0.25
Now with a note that starts at a position not in the hierarchy:
>>> split = MetricalSplitter(0.05, 2.0, start_hierarchy=start_hierarchy)
>>> split.start_duration_pairs
[(0.05, 0.2), (0.25, 0.25), (0.5, 0.5), (1.0, 1.0), (2.0, 0.05)]
Source code in amads/time/meter/break_it_up.py
171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 | |
grid
¶
The module seeks to find the smallest metrical pulse level (broadly, “tatum”) in response to a source and user tolerance settings.
In the simplest case, a source records its metrical positions exactly, including fractional values as needed. We provide functionality for standard, general algorithms in these cases (greatest common denominator and fraction estimation) which are battle-tested and computationally efficient.
In metrically simple and regular cases like chorales, this value might be the eighth note, for instance. In other cases, it gets more complex. For example, Beethoven's Opus 10 Nr.2 Movement 1 includes a triplet 16th turn figure in measure 1 (tatum = 1/6 division of the quarter note) and also dotted rhythms that pair a dotted 16th with a 32nd note from measure 5 (tatum = 1/8 division of the quarter). So to catch these cases in the first 5 measures, we need the lowest common multiple of 6 and 8, i.e., 24 per quarter (or 48 bins per 2/4 measure).
In cases of extreme complexity, there may be a “need” for a considerably shorter tatum pulse (and, equivalently, a greater number of bins). This is relevant for some modern music, as well as cases where grace notes are assigned a specific metrical position/duration (though in many encoded standards, grace notes are not assigned separate metrical positions).
Moreover, there are musical sources that do not encode fractional time values, but rather approximation with floats. These include any:
- frame-wise representations of time (including MIDI and any attempted transcription from audio),
- processing via code libraries that likewise convert fractions to floats,
- secondary representations like most CSVs.
As division by 3 leads to rounding, approximation, and floating point errors, and as much music involves those divisions, this is widely relevant.
The standard algorithms often fail in these contexts, largely because symbolic music tends to prioritise certain metrical divisions over others. For example, 15/16 is a commonly used metrical position (largely because 16 is a power of 2), but 14/15 is not. That being the case, while 14/15 might be a better mathematical fit for approximating a value, it is typically incorrect as the musical solution. We can use the term “incorrect” advisedly here because the floats are secondary representations of a known fractional ground truth. Doctests demonstrate some of these cases.
Author: Mark Gotham
Functions¶
starts_to_int_relative_counter
¶
starts_to_int_relative_counter(
starts: Iterable[float],
modulo_reference: int = 1,
decimal_places: int = 5,
)
Find and count all fractional parts of an iterable.
Simple wrapper function to create a Counter (dict) that (by default) maps the fractional parts of starts to the number of occurrences of that fraction. In this default case, default the calculation is bascially $start - int(start)$, so starts 1.5 and 2.5 map to 0.5.
Alternatively, the modulo reference value does not need to be 1.
Set a different modulo_reference value
to use this for measuring distance since the beginning of measure,
for example.
Fractional parts are rounded to decimal_places decimal points (default 5),
which gives a tolerance down to 0.00001 and accommodates common musical
fractions such as thirds (0.33333) and sixths (0.16667).
Examples:
>>> test_list = [0.0, 0.0, 0.5, 1.0, 1.5, 1.75, 2.0, 2.3333333333, 2.666667, 3.00000000000000001]
>>> starts_to_int_relative_counter(test_list)
Counter({0.0: 5, 0.5: 2, 0.75: 1, 0.33333: 1, 0.66667: 1})
And now with modulo_reference
>>> starts_to_int_relative_counter(test_list, modulo_reference=2)
Counter({0.0: 3, 1.0: 2, 0.5: 1, 1.5: 1, 1.75: 1, 0.33333: 1, 0.66667: 1})
Source code in amads/time/meter/grid.py
65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 | |
approximate_pulse_match_with_priority_list
¶
approximate_pulse_match_with_priority_list(
x: float,
distance_threshold: float = 0.001,
pulse_priority_list: Optional[list] = None,
) -> Optional[Fraction]
Takes a float and an ordered list of possible pulses, returning the first pulse in the list to approximate the input float.
This is a new function by MG as reported in [1].
Parameters:
-
x(float) –Input value to be approximated as a fraction.
-
distance_threshold(float, default:0.001) –The distance threshold.
-
pulse_priority_list(list[Fraction], default:None) –Ordered list of pulse values to try. If unspecified, this defaults to 4, 3, 2, 1.5, 1, and the default output of
generate_n_smooth_numbers.
Returns:
-
Union(None, Fraction)–None for no match, or a Fraction(numerator, denominator).
Raises:
-
ValueError–If
pulse_priority_listcontains 0 or None.
References
[1] Gotham, Mark R. H. (2025). Keeping Score: Computational Methods for the Analysis of Encoded ("Symbolic") Musical Scores (v0.3+) Zenodo. https://doi.org/10.5281/zenodo.14938027
Examples:
>>> approximate_pulse_match_with_priority_list(5/6)
Fraction(1, 6)
>>> test_case = round(float(11/12), 5)
>>> test_case
0.91667
>>> approximate_pulse_match_with_priority_list(test_case)
Fraction(1, 12)
Note that Fraction(1, 12) is included in the default list,
while Fraction(11, 12) is not as that would be an extremely unusual tatum value.
If the distance_threshold is very coarse, expect errors:
>>> approximate_pulse_match_with_priority_list(29 + 1/12, distance_threshold=0.1)
Fraction(1, 1)
>>> approximate_pulse_match_with_priority_list(29 + 1/12, distance_threshold=0.01)
Fraction(1, 12)
Source code in amads/time/meter/grid.py
107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 | |
generate_n_smooth_numbers
¶
generate_n_smooth_numbers(
bases: list[int] = [2, 3], max_value: int = 100, invert: bool = True
) -> list
Generates a sorted list of "N-smooth" numbers up to a specified maximum value.
An N-smooth number is a positive integer whose prime factors are all
less than or equal to the largest number in the bases list.
Parameters:
-
max_value(int, default:100) –The maximum value to generate numbers up to. Defaults to 100.
-
bases(list, default:[2, 3]) –A list of base values (integers > 1) representing the maximum allowed prime factor. Defaults to [2, 3].
-
invert(bool, default:True) –If True, return Fraction(1, x) for each smooth number x instead of x itself. Defaults to True.
Returns:
-
list–A sorted list of N-smooth numbers (or their reciprocals if
invert=True).
Raises:
-
ValueError–If
basescontains non-integers or values <= 1, or ifmax_valueis not a positive integer.
Examples:
Our metrical default:
>>> generate_n_smooth_numbers(invert=False) # all defaults `max_value=100`, `bases [2, 3]`
[1, 2, 3, 4, 6, 8, 9, 12, 16, 18, 24, 27, 32, 36, 48, 54, 64, 72, 81, 96]
Other cases:
>>> generate_n_smooth_numbers(max_value=10, bases=[2], invert=False)
[1, 2, 4, 8]
>>> generate_n_smooth_numbers(max_value=20, bases=[2, 3], invert=False)
[1, 2, 3, 4, 6, 8, 9, 12, 16, 18]
>>> generate_n_smooth_numbers(max_value=50, bases=[2, 3, 5], invert=False)
[1, 2, 3, 4, 5, 6, 8, 9, 10, 12, 15, 16, 18, 20, 24, 25, 27, 30, 32, 36, 40, 45, 48, 50]
By default, invert is True:
>>> generate_n_smooth_numbers()[-1]
Fraction(1, 96)
Source code in amads/time/meter/grid.py
196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 | |
is_genuine_float
¶
is_genuine_float(num)
Distinguish de facto numeric type, particularly for X.0 integers.
This utility function may move position in code base.
Parameters:
-
num(float) –
Examples:
Float type and no equivalent int.
>>> is_genuine_float(5.5)
True
Float type, but equivalent int.
>>> is_genuine_float(5.0)
False
Any fraction, equivalent int or otherwise
>>> is_genuine_float(Fraction(9, 2))
False
>>> is_genuine_float(Fraction(10, 2))
False
Obligatory edge case checks.
>>> is_genuine_float(0.0)
False
Obligatory edge case checks.
>>> is_genuine_float(None)
False
Source code in amads/time/meter/grid.py
271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 | |
get_tatum_from_priorities
¶
get_tatum_from_priorities(
starts: Sequence,
pulse_priority_list: Optional[list] = None,
distance_threshold: float = 1 / 24,
proportion_threshold: Optional[float] = 0.999,
) -> Fraction
Estimate metrical positions from floats.
This function serves cases where temporal position values are defined relative to some origin, such as the time elapsed since:
- the start of a piece (or section) in quarter notes (or some other consistent symbolic value)
- the start of a measure (or other container), assuming those measures are of a constant duration.
Use cases include the attempted retrieval of true metrical positions (fractions) from rounded versions thereof (floats). See also notes at the top of this module for why standard algorithms fail at this task in a musical setting.
This function serves those common cases where there is a need to balance between capturing event positions as accurately as possible while not making excessive complexity to account for a few anomalous notes. Most importantly, it enables the explicit prioritisation of common pulse divisions. Defaults prioritse 16x divsion over 15x, for example.
Parameters:
-
starts(Sequence) –Any sequence giving the starting position of events. Each constituent start must be expressed relative to a reference value such that X.0 is the start of a unit, X.5 is the mid-point, etc. Floats are the main expected type here (as above); we seek to reverse engineer a plausible fraction from each. If any start is already an exact Fraction or int, then it stays as it is, whatever the user setting: this functionality serves to improve the accuracy of timing data; there's no question of ever reducing it, even if user settings suggest that.
-
pulse_priority_list(Optional[list], default:None) –The point of this function is to encode musically common pulse values. This argument defaults to numbers under 100 with prime factors of only 2 and 3 ("3-smooth"), in increasing order. The user can define any alternative list, optionally making use of
generate_n_smooth_numbersfor the purpose. See notes atapproximate_fraction_with_priorities. Make sure this list is exhaustive: the function will raise an error if no match is found. -
distance_threshold(float, default:1 / 24) –The rounding tolerance between a temporal position multiplied by the bin value and the nearest integer. This is essential when working with floats. Defaults to 1/24, but can be set to any value.
-
proportion_threshold(Optional[float], default:0.999) –Optionally, set a proportional number of events notes to account for. The default of .999 means that once at least 99.9% of the source's notes are handled, we ignore the rest. This is achieved by iterating through the fractional-part Counter of float values (from most to least common) and stopping early once the cumulative proportion of events accounted for exceeds the threshold. This option should be chosen with care as, in this case, only the unit value and equal divisions thereof are considered.
Examples:
A simple case, expressed in different ways.
>>> tatum_1_6 = [0, 1/3, Fraction(1, 2), 1]
>>> get_tatum_from_priorities(tatum_1_6)
Fraction(1, 6)
>>> tatum_1_6 = [0, 0.333, 0.5, 1]
>>> get_tatum_from_priorities(tatum_1_6)
Fraction(1, 6)
An example of values from the BPSD dataset (Zeitler et al.).
>>> from amads.time.meter import profiles
>>> bpsd_Op027No1 = profiles.BPSD().op027No1_01 # /16 divisions of the measure and /12 too (from m.48). Tatum 1/48
>>> get_tatum_from_priorities(bpsd_Op027No1, distance_threshold=1/24)
Fraction(1, 48)
Change the distance_threshold
>>> get_tatum_from_priorities(bpsd_Op027No1, distance_threshold=1/6)
Fraction(1, 12)
Change the proportion_threshold:
>>> get_tatum_from_priorities(bpsd_Op027No1, distance_threshold=1/24, proportion_threshold=0.5)
Fraction(1, 24)
>>> get_tatum_from_priorities(bpsd_Op027No1, distance_threshold=1/24, proportion_threshold=0.9)
Fraction(1, 48)
This also works without any floats (and therefore, no priorities needed)
>>> get_tatum_from_priorities([1, 3])
Fraction(1, 1)
>>> get_tatum_from_priorities([0, 3])
Fraction(3, 1)
>>> get_tatum_from_priorities([0, Fraction(1, 3), Fraction(4, 6)])
Fraction(1, 3)
>>> get_tatum_from_priorities([28.0, 29.0, 29.5, 30.0, 32.0, 33.0, 34.0, 36.0, 38.0, 40.0])
Fraction(1, 2)
Source code in amads/time/meter/grid.py
311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 | |
profiles
¶
Profiles of metrical position usage provided or deduced from the literature.
See the code for details. Data includes:
- WorldSample16
- shiko
- son
- rumba
- soukous
- gahu
- bossa_nova
- WorldSample12
- soli
- tambú
- bembé
- bembé_2
- yoruba
- tonada
- asaadua
- sorsonet
- bemba
- ashanti
- BPSD (Beethoven piano sonata dataset)
- op002No2_01
- op054_01
- op027No1_01
- op049No1_01
- op013_01
- op101_01
- op111_01
- op031No2_01
- op014No1_01
- op010No2_01
- op007_01
- op078_01
- op109_01
- op081a_01
- op028_01
- op002No3_01
- op010No1_01
- op090_01
- op031No1_01
- op014No2_01
- op010No3_01
- op031No3_01
- op110_01
- op026_01
- op049No2_01
- op002No1_01
- op027No2_01
- op057_01
- op053_01
- op022_01
- op106_01
- op079_01
- all
Author: Mark Gotham
representations
¶
This module serves to map out metrical hierarchies in a number of different ways and to express the relationship between the notes in and the hierarchy of a metrical cycle.
Uses include identifying notes that traverse metrical levels, for analysis (e.g., as a cycle of syncopation) and notation (e.g., re-notating to reflect the within-cycle notational conventions).
Author: Mark Gotham
Classes¶
StartTimeHierarchy
¶
StartTimeHierarchy(
start_hierarchy: list[list], names: Optional[dict] = None
)
Encoding metrical structure as a hierarchy of start times.
A representation of metrical levels in terms of starts expressed by quarter length from the start of the cycle.
Parameters:
-
start_hierarchy(list[list]) –Users can specify the
start_hierarchydirectly and completely from scratch. Use this for advanced, non-standard metrical structures including those without 2-/3- grouping, or even nested hierarchies, as well as for (optionally) encoding micro-timing directly into the metrical structure. The only "well-formed" criteria we expect are use of 0.0 and full cycle length at the top level, and presence of all timepoints from one level in each subsequent level. For creating this information from pulse lengths, time signatures, and more see theto_start_hierarchymethods on those classes. -
names(Optional[dict], default:None) –Optionally create a dict mapping temporal positions to names. Currently, this supports one textual value per temporal position (key), e.g., {0.0: "ta", 1.0: "ka", 2.0: "di", 3.0: "mi"}.
Attributes:
-
start_hierarchy(list[list]) –This is the main structured data for the class.
-
cycle_length(float) –Derived from
[0][-1]of the givenstart_hierarchy -
pulse_lengths(list or None) –None until if/when
compute_pulse_lengths()is called explicitly.
Source code in amads/time/meter/representations.py
150 151 152 153 154 155 156 157 158 159 160 161 162 163 | |
Functions¶
coincident_pulse_list
¶
coincident_pulse_list(granular_pulse: float) -> list
Create a flat list setting out the number of intersecting pulses at each successive position in a metrical cycle.
For example, the output [4, 1, 2, 1, 3, 1, 2, 1] refers to a base pulse unit of 1, with addition pulse streams accenting every 2nd, 4th, and 8th position.
Parameters:
-
granular_pulse(float) –The pulse value of the fastest level to consider e.g., 1, or 0.25.
Examples:
You can currently set the granular_pulse value to anything (this may change).
For instance, in the pair of examples below,
first we have a granular_pulse that's present in the input,
and then a case using a faster level that's not present (this simply pads the data out):
>>> hierarchy = StartTimeHierarchy([[0.0, 4.0], [0.0, 2.0, 4.0], [0.0, 1.0, 2.0, 3.0, 4.0]])
>>> hierarchy.coincident_pulse_list(granular_pulse=1)
[3, 1, 2, 1]
Now, changing the granular_pulse for a bit of over-sampling:
>>> hierarchy.coincident_pulse_list(granular_pulse=0.5)
[3, 0, 1, 0, 2, 0, 1, 0]
Source code in amads/time/meter/representations.py
165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 | |
compute_pulse_lengths
¶
compute_pulse_lengths() -> list
Check if levels have a regular pulse and if so, return the pulse length value.
Also sets self.pulse_lengths as a side effect.
Returns:
-
list–Returns a list of pulse values corresponding to the start hierarchy, of the same length. If a level is not regular, the list is populated with None.
Examples:
>>> hierarchy = StartTimeHierarchy([[0.0, 4.0], [0.0, 2.0, 4.0], [0.0, 1.0, 2.0, 3.0, 4.0]])
>>> hierarchy.compute_pulse_lengths()
[4.0, 2.0, 1.0]
>>> uneven = StartTimeHierarchy([[0.0, 4.0], [0.0, 3.0, 4.0], [0.0, 1.0, 2.0, 3.0, 4.0]])
>>> uneven.compute_pulse_lengths()
[4.0, None, 1.0]
Source code in amads/time/meter/representations.py
216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 | |
add_faster_levels
¶
add_faster_levels(minimum_beat_type: int = 64)
Recursively add faster levels until the minimum_beat_type value
The minimum_beat_type is subject to the same constraints as the beat_types ("denominators")
i.e., powers of 2 (1, 2, 4, 8, 16, 32, 64, ...).
The default = 64 for 64th note.
Parameters:
-
minimum_beat_type(int, default:64) –Recursively create further levels down to this value. Must be power of two. Defaults to 64 for 64th notes.
Raises:
-
ValueError–if the currently fastest level of a
starts_hierarchyis not periodic, or if either of the fastest level orminimum_beat_typeare not powers of 2. Set thestarts_hierarchymanually in these non-standard cases.
Examples:
>>> hierarchy = StartTimeHierarchy([[0.0, 4.0], [0.0, 2.0, 4.0]])
>>> hierarchy.start_hierarchy
[[0.0, 4.0], [0.0, 2.0, 4.0]]
>>> hierarchy.compute_pulse_lengths()
[4.0, 2.0]
>>> hierarchy.add_faster_levels(minimum_beat_type=4)
>>> hierarchy.start_hierarchy
[[0.0, 4.0], [0.0, 2.0, 4.0], [0.0, 1.0, 2.0, 3.0, 4.0]]
>>> hierarchy.pulse_lengths
[4.0, 2.0, 1.0]
>>> hierarchy.add_faster_levels(minimum_beat_type=8)
>>> hierarchy.start_hierarchy[-1]
[0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0]
>>> len(hierarchy.start_hierarchy)
4
>>> hierarchy.pulse_lengths
[4.0, 2.0, 1.0, 0.5]
Source code in amads/time/meter/representations.py
251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 | |
TimeSignature
¶
TimeSignature(
beats: Optional[tuple[int]] = None,
beat_type: Optional[int] = None,
as_string: Optional[str] = None,
)
Represent the notational time signature object.
Parameters:
-
beats(Optional[tuple[int]], default:None) –The "numerator" of the time signature: beats per cycle, a number (int or fraction) or a lists thereof.
-
beat_type(Optional[int], default:None) –the so-called "denominator" of the time signature: a whole number power of 2 (1, 2, 4, 8, 16, 32, 64, ...). No so-called "irrational" meters yet (e.g., 2/3), sorry!
-
as_string(Optional[str], default:None) –An alternative way of creating this object from a string representation. See notes at
TimeSignature.from_string.
Source code in amads/time/meter/representations.py
356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 | |
Functions¶
from_string
¶
from_string()
Given a signature string, extract the constituent parts and create an object.
The string must take the form <beat>/<beat_type>
with exactly one "/" separating the two (spaces are ignored).
The string does not change.
The <beat> ("numerator") part may be a number (including 5 and 7 which
are supported) or more than one number separated by the "+" symbol.
For example, when encoding "5/4", use the total value only to avoid
segmentation above the denominator level ("5/4")
or the X+Y form to explicitly distinguish between "2+3" and "3+2".
I.e., "5/" time signatures have no 3+2 or 2+3 division by default.
See examples on TimeSignature.to_starts_hierarchy.
Finally, although we support and provide defaults for time signatures in the form "2+3/8", there is no such support for more than one "/" (i.e., the user must build cases like "4/4 + 3/8" explicitly according to how they see it).
Examples:
>>> ts_4_4 = TimeSignature(as_string="4/4")
>>> ts_4_4.beats # Tuple of one element
(4,)
>>> ts_4_4.beat_type
4
Source code in amads/time/meter/representations.py
374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 | |
check_valid
¶
check_valid()
Check the validity of the input.
.beatsmust be an integer or a list/tuple thereof..beat_typemust be a single integer power of two.
Examples:
>>> TimeSignature(as_string="4/3")
Traceback (most recent call last):
...
ValueError: Beat type set as 3 is invalid: must be a non-negative integer power of 2.
Source code in amads/time/meter/representations.py
410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 | |
fill_2s_3s
¶
fill_2s_3s()
Optionally, add pulse values to follow the conventions of the time signatures.
Enforcing 2- and 3-grouping, this only applies to cases with a single beat in the "numerator". For instance, given a "4/4" signature, this method will add the half-cycle (pulse value 2.0), given a "6/8", it will again add the half-cycle (pulse value 1.5), and given a "12/8", it will add both the half- and quarter-cycle (pulse values 3.0 and 1.5),
This functionality is factored out and does not run by default.
Even if this runs, the original time signature string is unchanged,
as is the beats attribute.
Examples:
>>> ts_4_4 = TimeSignature(as_string="4/4")
>>> ts_4_4.pulses
[4.0, 1.0]
>>> ts_4_4.fill_2s_3s()
>>> ts_4_4.pulses
[4.0, 2.0, 1.0]
>>> ts_6_8 = TimeSignature(as_string="6/8")
>>> ts_6_8.pulses
[3.0, 0.5]
>>> ts_6_8.fill_2s_3s()
>>> ts_6_8.pulses
[3.0, 1.5, 0.5]
Source code in amads/time/meter/representations.py
479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 | |
to_start_hierarchy
¶
to_start_hierarchy() -> list
Create a start hierarchy for almost any time signature
(with constraints as noted in the top level class description
and in the .from_string method).
See below for several examples of how this handles
specific time signatures and related assumptions,
and note the effect of running fill_2s_3s().
Returns:
-
list–Returns a list of lists with start positions by level.
Examples:
>>> ts_4_4 = TimeSignature(as_string="4/4")
>>> ts_4_4.pulses
[4.0, 1.0]
>>> test_1 = ts_4_4.to_start_hierarchy()
>>> test_1[0]
[0.0, 4.0]
>>> test_1[1]
[0.0, 1.0, 2.0, 3.0, 4.0]
>>> ts_4_4.fill_2s_3s()
>>> ts_4_4.pulses
[4.0, 2.0, 1.0]
>>> test_2 = ts_4_4.to_start_hierarchy()
>>> test_2[0]
[0.0, 4.0]
>>> test_2[1]
[0.0, 2.0, 4.0]
>>> test_2[2]
[0.0, 1.0, 2.0, 3.0, 4.0]
>>> ts_2_2 = TimeSignature(as_string="2/2")
>>> ts_2_2.pulses
[4.0, 2.0]
>>> test_3 = ts_2_2.to_start_hierarchy()
>>> test_3[0]
[0.0, 4.0]
>>> test_3[1]
[0.0, 2.0, 4.0]
>>> ts_2_2.fill_2s_3s() # no effect, unchanged
>>> ts_2_2.pulses
[4.0, 2.0]
>>> test_4 = ts_2_2.to_start_hierarchy()
>>> test_4[0]
[0.0, 4.0]
>>> test_4[1]
[0.0, 2.0, 4.0]
>>> ts_6_8 = TimeSignature(as_string="6/8")
>>> ts_6_8.pulses
[3.0, 0.5]
>>> test_5 = ts_6_8.to_start_hierarchy()
>>> test_5[0]
[0.0, 3.0]
>>> test_5[1]
[0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0]
>>> ts_6_8.fill_2s_3s()
>>> ts_6_8.pulses
[3.0, 1.5, 0.5]
>>> test_6 = ts_6_8.to_start_hierarchy()
>>> test_6[0]
[0.0, 3.0]
>>> test_6[1]
[0.0, 1.5, 3.0]
>>> test_6[2]
[0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0]
>>> ts_5_4 = TimeSignature(as_string="5/4")
>>> ts_5_4.pulses
[5.0, 1.0]
>>> test_7 = ts_5_4.to_start_hierarchy()
>>> test_7[0]
[0.0, 5.0]
>>> test_7[1]
[0.0, 1.0, 2.0, 3.0, 4.0, 5.0]
>>> ts_2_3_4 = TimeSignature(as_string="2+3/4")
>>> ts_2_3_4.pulses # as before
[5.0, 1.0]
>>> test_8 = ts_2_3_4.to_start_hierarchy()
>>> test_8[0]
[0.0, 5.0]
>>> test_8[1]
[0.0, 2.0, 5.0]
>>> test_8[2]
[0.0, 1.0, 2.0, 3.0, 4.0, 5.0]
Source code in amads/time/meter/representations.py
523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 | |
PulseLengths
¶
PulseLengths(
pulse_lengths: list[float],
cycle_length: Optional[float] = None,
include_cycle_length: bool = True,
)
Parameters:
-
pulse_lengths(list[float]) –Any valid list of pulse lengths, e.g., [4, 2, 1].
-
cycle_length(Optional[float], default:None) –Optional. If not provided, the cycle length is taken to be given by the longest pulse length.
-
include_cycle_length(bool, default:True) –Defaults to True. If True, when converting to starts, include the full cycle length in the list.
Source code in amads/time/meter/representations.py
662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 | |
Functions¶
to_start_hierarchy
¶
to_start_hierarchy(require_2_or_3_between_levels: bool = False)
Convert a list of pulse lengths into a corresponding list of lists.
Gives start positions per metrical level. All values (pulse lengths, start positions, and cycle_length) are all expressed in terms of quarter length.
That is, the user provides pulse lengths for each level of a metrical hierarchy, and the algorithm expands this into a hierarchy assuming equal spacing (aka "isochrony").
This does not work for ("nonisochronous") pulse streams of varying duration in time signatures like 5/x, 7/x (e.g., the level of 5/4 with dotted/undotted 1/2 notes).
It is still perfectly fine to use this for the pulse streams within those meters that are regular, equally spaced ("isochronous") (e.g., the 1/4 note level of 5/4).
The list of pulse lengths is handled internally in decreasing order, whatever the ordering in the argument.
If require_2_or_3_between_levels is True (default), this functions
checks that each level is either a 2 or 3 multiple of the next.
By default, the cycle_length is taken by the longest pulse length. Alternatively, this can be user-defined to anything as long as it is
- longer than the longest pulse and
- if
require_2_or_3_between_levelsis True then exactly 2x or 3x longer.
Parameters:
-
require_2_or_3_between_levels(bool, default:False) –Defaults to False. If True, raise a ValueError in the case of this condition not being met.
Returns:
-
list–Returns a list of lists with start positions by level.
Examples:
>>> qsl = PulseLengths(pulse_lengths=[4, 2, 1, 0.5])
>>> qsl.pulse_lengths
[4, 2, 1, 0.5]
>>> start_hierarchy = qsl.to_start_hierarchy()
>>> start_hierarchy[0]
[0.0, 4.0]
>>> start_hierarchy[1]
[0.0, 2.0, 4.0]
>>> start_hierarchy[2]
[0.0, 1.0, 2.0, 3.0, 4.0]
>>> start_hierarchy[3]
[0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0]
Source code in amads/time/meter/representations.py
698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 | |
one_pulse_to_start_hierarchy_list
¶
one_pulse_to_start_hierarchy_list(pulse_length: float)
Convert a single pulse length and cycle length into a list of starts. All expressed in quarter length.
Note:
A maximum of 4 decimal places is hardcoded to avoid accumulated floating-point
error from repeated addition. np.arange with float steps has the same
problem (its output requires identical rounding), so plain iteration + round()
is preferred here to keep the dependency-free path simple.
4 decimal points should be sufficient for all realistic use cases.
Parameters:
-
pulse_length(float) –The quarter length of the pulse (note: must be shorter than the
cycle_length).
Examples:
>>> pls = PulseLengths(pulse_lengths=[4, 2, 1, 0.5], cycle_length=4)
>>> pls.pulse_lengths
[4, 2, 1, 0.5]
>>> pls.one_pulse_to_start_hierarchy_list(1)
[0.0, 1.0, 2.0, 3.0, 4.0]
>>> pls = PulseLengths(pulse_lengths=[4, 2, 1, 0.5], cycle_length=4, include_cycle_length=False)
>>> pls.one_pulse_to_start_hierarchy_list(1)
[0.0, 1.0, 2.0, 3.0]
Source code in amads/time/meter/representations.py
790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 | |
to_array
¶
to_array()
Create an array with the levels as rows, and the entries as pulse length values, where those pulse lengths start, and 0 otherwise.
Examples:
>>> pls = PulseLengths(pulse_lengths=[4, 2, 1, 0.5], cycle_length=4)
>>> pls.to_array()
array([[4. , 0. , 0. , 0. , 0. , 0. , 0. , 0. ],
[2. , 0. , 0. , 0. , 2. , 0. , 0. , 0. ],
[1. , 0. , 1. , 0. , 1. , 0. , 1. , 0. ],
[0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5]])
Source code in amads/time/meter/representations.py
837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 | |
BeatPattern
¶
BeatPattern(beat_list: tuple[int, ...], beat_type: int)
Encoding only the part of a metrical structure identified as the beat pattern.
Parameters:
-
beat_list(tuple[int, ...]) –An ordered list of the beat types.
-
beat_type(int) –The lower value of a time signature to set the pulse value.
Source code in amads/time/meter/representations.py
887 888 889 890 891 892 893 894 | |
Functions¶
beat_pattern_to_start_hierarchy
¶
beat_pattern_to_start_hierarchy(
include_cycle_length: bool = True,
) -> list
Converts a list of beats
like [2, 2, 2]
or [3, 3]
or indeed
[6, 9]
into a list of within-cycle starting positions, as defined relative
to the start of the cycle.
Basically, the list of beats functions like the time signature's
so-called "numerator",
so for instance, [2, 2, 3] with the denominator 4 is a kind of 7/4.
This equates to starting positions of
[0.0, 2.0, 4.0, 7.0].
Parameters:
-
include_cycle_length(bool, default:True) –If True (default) then each level ends with the full cycle length (i.e., the start of the next cycle).
Examples:
>>> bp = BeatPattern((2, 2, 3), 4)
>>> bp.beat_pattern_to_start_hierarchy()
[0.0, 2.0, 4.0, 7.0]
>>> bp.beat_pattern_to_start_hierarchy(include_cycle_length = False)
[0.0, 2.0, 4.0]
Source code in amads/time/meter/representations.py
896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 | |
VariableNesting
¶
VariableNesting(nested: list)
Support the encoding of variably-nested list of numeric positions.
Main operations are
1. to store this data as is
2. to convert to start_hierarchy for inclusion in shared processes.
Parameters:
-
nested(list) –A list whose elements are either numeric values or (recursively) further nested lists.
Source code in amads/time/meter/representations.py
957 958 959 | |
Attributes¶
start_hierarchy
property
¶
start_hierarchy: list[list[float]]
Compute and cache the start hierarchy on first access.
Functions¶
to_start_hierarchy
¶
to_start_hierarchy() -> list[list[float]]
Convert a variably-nested list of numeric positions
into a start_hierarchy compatible with MetricalSplitter.
Nesting depth encodes metrical level similarly to elsewhere on this module: the top-level is the coarsest level, and each additional layer of nesting introduces one finer level.
Most importantly, subdivision is local: a sub-list applies only to that region, and the resulting hierarchy levels are padded with the surrounding coarse positions so that every level spans the full cycle.
All values must be strictly monotonically increasing when the structure is fully flattened.
Returns:
-
list[list[float]]–A
start_hierarchyordered from coarsest (index 0) to finest (index -1), suitable for passing directly toMetricalSplitter.
Raises:
-
ValueError–If the flattened values are not strictly monotonically increasing.
-
ValueError–If no positions are found in the nested structure.
Notes
Depth gaps are collapsed silently: if nesting exists at depths 0 and 2 but not at depth 1, the output still contains contiguous levels with no phantom intermediate entries.
See Also
MetricalSplitter: recipient of the start_hierarchy representation produced by this function.
PulseLengths.to_start_hierarchy: Alternative constructor for periodic meters.
TimeSignature.to_start_hierarchy: Alternative constructor from a time signature string.
Examples:
Relatively trivial case first. Where the input is flat, we still return a two-level hierarchy, adding the level[0] from position 0.0 to the cycle span length.
>>> VariableNesting([0, 12, 24, 36, 48]).to_start_hierarchy()
[[0.0, 48.0], [0.0, 12.0, 24.0, 36.0, 48.0]]
Now for one level of local nesting — coarse positions plus local refinement:
>>> VariableNesting([[0, 4, 8], 12, 24]).to_start_hierarchy()
[[0.0, 24.0], [0.0, 12.0, 24.0], [0.0, 4.0, 8.0, 12.0, 24.0]]
Here is a similar example, but with the nesting away from the initial position.
>>> VariableNesting([0, [4, 8], 12, 24]).to_start_hierarchy()
[[0.0, 24.0], [0.0, 4.0, 12.0, 24.0], [0.0, 4.0, 8.0, 12.0, 24.0]]
Mixed depth — subdivision appears only where specified:
>>> VariableNesting([0, 12, [24, [26, 28], 30], 36]).to_start_hierarchy()
[[0.0, 36.0], [0.0, 12.0, 24.0, 36.0], [0.0, 12.0, 24.0, 26.0, 30.0, 36.0], [0.0, 12.0, 24.0, 26.0, 28.0, 30.0, 36.0]]
And finally, the real case of Jeongganbo from the "Keeping Score" book (example 3.1).
>>> jg = [[0, 4, 8], 12, 24, 36, [48, 52, 56], 60, 72, [84, [88, 90], 92], 96, [108, 112, 116], 120]
>>> jg_start_h = VariableNesting(jg).to_start_hierarchy()
>>> jg_start_h[0] # start and end only
[0.0, 120.0]
>>> jg_start_h[2] # all apart from the one case of double division at position 90
[0.0, 4.0, 8.0, 12.0, 24.0, 36.0, 48.0, 52.0, 56.0, 60.0, 72.0, 84.0, 88.0, 92.0, 96.0, 108.0, 112.0, 116.0, 120.0]
>>> jg_start_h[3] == jg_start_h[-1] # all elements, last level
True
Source code in amads/time/meter/representations.py
968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 | |
Functions¶
is_non_negative_integer_power_of_two
¶
is_non_negative_integer_power_of_two(n: float) -> bool
Checks if a number is a non-negative integer power of 2. I.e. 1, 2, 4, ...
Examples:
>>> is_non_negative_integer_power_of_two(0)
False
>>> is_non_negative_integer_power_of_two(0.5)
False
As "non-negative integer" suggests, 2^0 = 1 is included.
>>> is_non_negative_integer_power_of_two(1)
True
>>> is_non_negative_integer_power_of_two(2)
True
>>> is_non_negative_integer_power_of_two(3)
False
>>> is_non_negative_integer_power_of_two(4)
True
Invlid types return False.
>>> is_non_negative_integer_power_of_two("TEST")
False
Source code in amads/time/meter/representations.py
29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 | |
syncopation
¶
Measures of syncopation from the literature.
Author: Mark Gotham
Classes¶
SyncopationMetric
¶
SyncopationMetric(path_to_score: Optional[str] = None)
times and similar).
The parameters of this class allow users to run from a score (with onsets etc. deduced from there) or directly on their own data (the necessary parameters differ slightly for each method).
Parameters:
-
path_to_score(Optional[str], default:None) –Path to the score in any supported format (e.g., MusicXML). Deduce any necessary onsets, beats etc. from the score as calculated by Partitura. Warning: Partitura takes “beats” from time signatures denominators, e.g., 6/8 has 6 “beats” (not 2).
Source code in amads/time/meter/syncopation.py
16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | |
Functions¶
load_note_array_from_score
¶
load_note_array_from_score()
Parse a score and return Partitura's .note_array() with include_metrical_position=True.
This should cover the required information. The note array's fields includes several fields of which methods here use the following (in their words):
- 'onset_beat': onset time of the note in beats
- 'duration_beat': duration of the note in beats
These values are called in the form note_array["onset_beat"].
Source code in amads/time/meter/syncopation.py
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | |
weighted_note_to_beat_distance
¶
weighted_note_to_beat_distance(
onset_beats: Optional[list] = None,
) -> float
TODO: WIP - does not currently replicate answers in the literature; further investigation to follow
The weighted note-to-beat distance measure (WNBD) measures the distance between note starts and records the traversing of beats, and the distance to the nearest beat.
The authors clarify that “notes are supposed to end where the next note starts”, so we're working with the inter-note interval (INI), rather than the duration. Note that there are one fewer INI values than notes.
Among the limitations is the incomplete definition of “beat” and the agnostic view of metre: “By strong beats we just mean pulses.” (§3.4).
Parameters:
-
onset_beats(Optional[list], default:None) –User supplied data for the onset time of each note expressed in beats. Optional.
Returns:
-
WNBD value (float)–
Examples:
We use the example of the son clave
(also available from the meter.profiles module),
adapting to match presentation in the literature.
>>> son = [1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0]
>>> onset_beats = vector_to_onset_beat(vector=son, beat_unit_length=4)
>>> sm = SyncopationMetric()
>>> sm.weighted_note_to_beat_distance(onset_beats=onset_beats)
Fraction(14, 5)
>>> hesitation = [1, 0, 1, 0, 1, 0, 0, 1]
>>> onset_beats = vector_to_onset_beat(vector=hesitation, beat_unit_length=4)
>>> sm = SyncopationMetric()
>>> sm.weighted_note_to_beat_distance(onset_beats=onset_beats)
Fraction(1, 2)
>>> from amads.music import example
>>> test_xml_file = str(example.fullpath("musicxml/ex1.xml"))
>>> sm = SyncopationMetric(path_to_score=test_xml_file)
>>> sm.weighted_note_to_beat_distance()
Fraction(4, 3)
Source code in amads/time/meter/syncopation.py
62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 | |
Functions¶
vector_to_onset_beat
¶
vector_to_onset_beat(vector: list, beat_unit_length: int = 2)
Map from a vector to onset beat data via vector_to_multiset.
Examples:
>>> son = [1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1] # Final 1 for cycle rotation
>>> vector_to_onset_beat(vector=son, beat_unit_length=4) # NB different beat value
(Fraction(0, 1), Fraction(3, 4), Fraction(3, 2), Fraction(5, 2), Fraction(3, 1), Fraction(4, 1))
Source code in amads/time/meter/syncopation.py
168 169 170 171 172 173 174 175 176 177 178 179 180 | |
syncopation_span
¶
This module sets out a new "IOI span" measure of syncopation.
(Note that some alternative, existing measures of syncopation from the literature are available in
amads.time.meter.syncopation.).
Syncopation is defined here by the interaction between 1) a pair of consecutive onsets and 2) the surrounding hierarchy, particularly wrt positions during the span between the two onsets.
Here, a note event is syncopated if 1) it starts at a lower-weight metrical position and 2) the next onset occurs only after positions of strictly higher weight.
That is, a potential syncopation is set up and opportunities for resolving are not taken (missed/avoided).
That much is relatively clear. More complex and debatable is the scoring system to implement this. In the absense of empirical testing, some plausible starting point preliminaries. Currently, the score for a single onset pair (A, B) is calculated with two components: - count: number of higher-weight positions traversed in the span (A, B) - gap: difference between the maximum weight traversed and A's own weight
These two are combined and weighted by a decay factor to differentiate resolution by a nearby onset counts vs a distant one.
The total syncopation score for an onset (here A)
is the sum of decayed pair scores for events that are
- during the span (ending with event B)
- also during max_lookahead onsets (if B is further off).
All positions are expressed as absolute quarter-lengths from the start of the
first metrical cycle.
The metrical cycle wraps automatically (position mod cycle_length).
Dependencies
amads.time.meter.representations
Author: Mark Gotham
Classes¶
PairSyncopation
dataclass
¶
PairSyncopation(
onset_a: float,
onset_b: float,
weight_a: float,
max_weight_traversed: float,
count: int,
gap: float,
raw_score: float,
decay: float,
score: float,
)
Syncopation contribution from a single onset pair (A, B).
Attributes:
-
onset_a(float) –Absolute position of the first onset.
-
onset_b(float) –Absolute position of the second onset.
-
weight_a(float) –Metrical weight at onset_a (coincident pulse count by default).
-
max_weight_traversed(float) –Highest weight of any metrical position strictly inside (onset_a, onset_b).
-
count(int) –Number of positions inside (onset_a, onset_b) with weight > weight_a.
-
gap(float) –max_weight_traversed - weight_a (0 if no higher position traversed).
-
raw_score(float) –count * gap (0 if not syncopated).
-
decay(float) –Decay factor applied at this lookahead distance.
-
score(float) –raw_score * decay.
OnsetSyncopation
dataclass
¶
OnsetSyncopation(
onset: float,
weight: int,
pairs: list[PairSyncopation] = list(),
score: float = 0.0,
)
Aggregated syncopation score for a single onset, across all lookahead pairs.
Attributes:
-
onset(float) –Absolute position of this onset.
-
weight(float) –Metrical weight at this position.
-
pairs(list[PairSyncopation]) –One entry per lookahead onset considered.
-
score(float) –Sum of pair scores.
SyncopationAnalysis
dataclass
¶
SyncopationAnalysis(
onset_results: list[OnsetSyncopation],
total_score: float,
hierarchy: StartTimeHierarchy,
)
Full syncopation analysis for a sequence of onsets.
Attributes:
-
onset_results(list[OnsetSyncopation]) –Per-onset breakdown.
-
total_score(float) –Sum of all onset scores.
-
hierarchy(StartTimeHierarchy) –The metrical hierarchy used.
Functions¶
weight_at_position
¶
weight_at_position(
position: float,
hierarchy: StartTimeHierarchy,
weight_map: dict[float, int],
level_weights: Optional[list[float]] = None,
) -> float
Return the (optionally scaled) metrical weight at an absolute position.
The position is reduced modulo the cycle length before lookup. If the position does not fall on a grid point, weight 0 is returned.
Note that this may be merged/refactored with MetricalSplitter.
Parameters:
-
position(float) –Absolute quarter-length position.
-
hierarchy(StartTimeHierarchy) – -
weight_map(dict) –Pre-built by
_build_weight_map. -
level_weights(list[float], default:None) –If provided, must have one entry per coincident-pulse-count level. The raw count is used as an index into this list (1-based). Defaults to the raw count (rank = 1 per level).
Returns:
-
float–Scaled weight (or raw int count if level_weights is None).
Examples:
>>> from amads.time.meter.representations import PulseLengths, StartTimeHierarchy
>>> pl = PulseLengths([4, 2, 1], cycle_length=4)
>>> sh = StartTimeHierarchy(pl.to_start_hierarchy())
>>> wm = _build_weight_map(sh, granular_pulse=1.0)
>>> weight_at_position(0.0, sh, wm)
3
>>> weight_at_position(2.0, sh, wm)
2
>>> weight_at_position(1.0, sh, wm)
1
>>> weight_at_position(0.5, sh, wm) # off-grid
0
Source code in amads/time/meter/syncopation_span.py
185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 | |
syncopation_for_pair
¶
syncopation_for_pair(
onset_a: float,
onset_b: float,
hierarchy: StartTimeHierarchy,
weight_map: dict[float, int],
level_weights: Optional[list[float]] = None,
decay: float = 1.0,
) -> PairSyncopation
Compute the syncopation contribution for a single onset pair (A, B).
A note starting at onset_a and sustained until onset_b is syncopated if the span (onset_a, onset_b) contains metrical positions of strictly higher weight than onset_a itself.
Parameters:
-
onset_a(float) –Absolute quarter-length positions; onset_b > onset_a.
-
onset_b(float) –Absolute quarter-length positions; onset_b > onset_a.
-
hierarchy(StartTimeHierarchy) – -
weight_map(dict) –Pre-built by
_build_weight_map. -
level_weights(list[float], default:None) –Custom importance scaling per weight level.
-
decay(float, default:1.0) –Decay factor for this lookahead distance. Defaults to 1.0.
Returns:
Examples:
>>> from amads.time.meter.representations import PulseLengths, StartTimeHierarchy
>>> pl = PulseLengths([4, 2, 1], cycle_length=4)
>>> sh = StartTimeHierarchy(pl.to_start_hierarchy())
>>> wm = _build_weight_map(sh, granular_pulse=1.0)
Not syncopated: starts on the downbeat (highest weight).
>>> pair = syncopation_for_pair(0.0, 2.0, sh, wm)
>>> pair.score
0.0
Syncopated: starts on weight-1 position, crosses weight-2 position.
>>> pair = syncopation_for_pair(1.0, 3.0, sh, wm)
>>> pair.gap
1
>>> pair.count
1
>>> pair.score
1.0
Strongly syncopated: starts on weight-1, crosses weight-3 (downbeat).
>>> pair = syncopation_for_pair(3.0, 5.0, sh, wm)
>>> pair.gap
2
>>> pair.count
1
>>> pair.score
2.0
Source code in amads/time/meter/syncopation_span.py
292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 | |
default_decay
¶
default_decay(n: int) -> float
Default decay function: 1/n where n is the lookahead index (1-based).
n=1 is the immediately following onset. This received the full (maximum) weight as the first opportunity to resolve. n=2 is the onset after that (half weight), etc.
Examples:
>>> default_decay(1)
1.0
>>> default_decay(2)
0.5
>>> default_decay(4)
0.25
Source code in amads/time/meter/syncopation_span.py
383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 | |
analyse
¶
analyse(
onsets: list[float],
hierarchy: StartTimeHierarchy,
granular_pulse: float,
level_weights: Optional[list[float]] = None,
decay_fn: Optional[Callable[[int], float]] = None,
max_lookahead: int = 1,
) -> SyncopationAnalysis
Analyse syncopation across a sequence of onsets against a metrical hierarchy.
For each onset A, the function looks ahead at the next max_lookahead onsets
(B, C, ...) and computes a decayed syncopation score for each pair (A, B),
(A, C), etc. The onset's total score is the sum of these pair scores.
Parameters:
-
onsets(list[float]) –Absolute quarter-length positions of note onsets, in order.
-
hierarchy(StartTimeHierarchy) –The metrical hierarchy against which to analyse.
-
granular_pulse(float) –Resolution of the metrical grid (e.g. 0.5 for eighth notes). Must divide the cycle length evenly.
-
level_weights(list[float], default:None) –Custom importance per weight level (index 0 = weight 1). Defaults to raw coincident pulse count (rank weighting).
-
decay_fn(callable, default:None) –Function mapping lookahead index n (1-based) to a decay scalar. Defaults to
default_decay(1/n). -
max_lookahead(int, default:1) –Number of subsequent onsets to consider for each onset A. Defaults to 1 (immediate successor only).
Returns:
Examples:
>>> from amads.time.meter.representations import PulseLengths, StartTimeHierarchy
>>> pl = PulseLengths([4, 2, 1], cycle_length=4)
>>> sh = StartTimeHierarchy(pl.to_start_hierarchy())
Simple case: four quarter notes. The onset at 1.0 (weight 1) resolves at 2.0 with nothing between — not syncopated at max_lookahead=1. With max_lookahead=2 it also considers pair (1.0, 3.0) which crosses weight-2 at position 2.0, syncopated with decay 1/2.
>>> result = analyse([0.0, 1.0, 2.0, 3.0], sh, granular_pulse=1.0, max_lookahead=2)
>>> result.onset_results[1].score # onset at 1.0: gap=1 * decay=0.5
0.5
>>> result.onset_results[0].score # onset at 0.0, highest weight
0.0
Source code in amads/time/meter/syncopation_span.py
408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 | |
transition_heatmap
¶
transition_heatmap(
hierarchy: StartTimeHierarchy,
granular_pulse: float,
level_weights: Optional[list[float]] = None,
) -> tuple[list[float], list[list[float]]]
Compute syncopation scores for all ordered pairs of grid positions (A, B).
The upper triangle (j > i) scores pairs within one cycle, where B follows A directly. The lower triangle (j < i) scores the wrap-around case: A is near the end of the cycle and B is the corresponding position in the next cycle, so the IOI is still at most one full cycle length. The diagonal (j == i) is always 0.0 (zero IOI, undefined).
Parameters:
-
hierarchy(StartTimeHierarchy) – -
granular_pulse(float) –Grid resolution; determines the set of positions considered.
-
level_weights(list[float], default:None) –
Returns:
-
positions(list[float]) –The grid positions (row/column labels).
-
matrix(list[list[float]]) –matrix[i][j] is the syncopation score for onset at positions[i] resolving at positions[j]. Diagonal entries are 0.0.
Examples:
>>> from amads.time.meter.representations import PulseLengths, StartTimeHierarchy
>>> pl = PulseLengths([4, 2, 1], cycle_length=4)
>>> sh = StartTimeHierarchy(pl.to_start_hierarchy())
>>> positions, matrix = transition_heatmap(sh, granular_pulse=1.0)
>>> positions
[0.0, 1.0, 2.0, 3.0]
>>> matrix[2][3] # upper: pos 2 -> pos 3, nothing higher between: not syncopated
0.0
>>> matrix[1][3] # upper: pos 1 -> pos 3, crosses weight-2 at pos 2
1.0
>>> matrix[3][1] # lower/wrap: pos 3 -> pos 1 next cycle, crosses downbeat (weight 3)
2.0
Source code in amads/time/meter/syncopation_span.py
500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 | |
plot_transition_heatmap
¶
plot_transition_heatmap(
hierarchy: StartTimeHierarchy,
granular_pulse: float,
level_weights: Optional[list[float]] = None,
mask_lower: bool = False,
vmax: Optional[float] = None,
title: str = "Syncopation Transition Heatmap",
write_not_show: bool = False,
) -> Tuple[figure, axes]
Plot the syncopation transition heatmap using matplotlib.
Each cell (i, j) shows the syncopation score for an onset pair starting at grid position i and ending at position j.
The upper triangle (j > i) scores pairs within one cycle.
The lower triangle (j < i) scores the wrap-around case: onset A is nearer-the end of the cycle than B so the span goes from A via cycle end to B within the next cycle (IOI still <= one full cycle).
The diagonal (j == i) is always blank.
Colour scheme made anew in this module. Similar to "YlOrRd" but with the zero-end not fully white (some distinction from BG).
Parameters:
-
hierarchy(StartTimeHierarchy) – -
granular_pulse(float) – -
level_weights(list[float], default:None) – -
mask_lower(bool, default:False) –If True, mask the lower triangle instead of showing wrap-around scores. Defaults to False (show the lower triangle).
-
vmax(float, default:None) –Colour scale maximum. Defaults to the maximum score in the data, computed per plot. Pass an explicit value for cross-meter comparison.
-
title(str, default:'Syncopation Transition Heatmap') –Plot title.
-
write_not_show(bool, default:False) –If True, write to local directory. If False, show (default for use in notebooks)
Returns:
-
fig, ax : matplotlib figure, matplotlib axes–Displays the plot via matplotlib.
Source code in amads/time/meter/syncopation_span.py
578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 | |
tatum
¶
Prepare a grid based on the tatum (smallest value needed to fully express all event positions). This will be a monotonically increasing sequence of integers where each integer expresses the event position relative in multiples of the tatum.
This is needed for algorithms including the Inner Metrical Analysis (IMA) which takes this list of integers indices and deduced local and spectral meters.
Author: Mark Gotham
Functions¶
score_to_offsets
¶
score_to_offsets(path_to_score: str, to_indices: bool = True) -> list
Import a score and convert it to the sorted list of unique starting timepoints as measured in quarters since the start of the score, and (optionally) convert those starts to indices on a tatum grid.
Note: score parsing warnings are supressed. If you need to test the validity of scores, handle that separately.
Parameters:
-
path_to_score(str) –A string for the file path or URL.
-
to_indices(bool, default:True) –If True, convert the starts to indices on a tatum grid.
Examples:
Two examples from "Species" Counterpoint. The first is straightforwardly in regular whole notes moving together, so the gaps are 4.0 apart (in "quarter notes") and the tatum is 4.
>>> score_path = "https://github.com/MarkGotham/species/raw/refs/heads/main/1x1/005.mxl"
>>> score_to_offsets(score_path, to_indices=False)
[0.0, 4.0, 8.0, 12.0, 16.0, 20.0, 24.0, 28.0, 32.0, 36.0, 40.0]
>>> score_to_offsets(score_path)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
The second example is from later in book 1. This is the first example of "florid" (5th species) counterpoint. There is one pair of eighth notes in this example (offsets 29.0, 29.5, 30.0) so the tatum is 0.5.
>>> url_5th_species = "https://github.com/MarkGotham/species/raw/refs/heads/main/1x1/082.mxl"
>>> starts = score_to_offsets(url_5th_species, to_indices=False)
This is how it starts:
>>> starts[:5]
[0.0, 2.0, 4.0, 5.0, 6.0]
And this is the part with the eight note pair:
>>> starts[22:]
[28.0, 29.0, 29.5, 30.0, 32.0, 33.0, 34.0, 36.0, 38.0, 40.0]
>>> indices = starts_to_indices(starts)
>>> indices[:5]
[0, 4, 8, 10, 12]
>>> indices = score_to_offsets(url_5th_species, to_indices=True)
>>> indices[:5]
[0, 4, 8, 10, 12]
>>> indices[22:]
[56, 58, 59, 60, 64, 66, 68, 72, 76, 80]
Source code in amads/time/meter/tatum.py
23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 | |
starts_to_indices
¶
starts_to_indices(
starts: list, tatum: Union[Fraction, int, None] = None
) -> list
Given a list of start times, convert to a list of indices on the tatum grid.
If a tatum value is provided, use that; otherwise, deduce the tatum using GCD methods.
This is the input format for the IMA algorithm, among others.
Note that this function is generally applicable to any contiainer. While taking time since the start of the whole piece may be the most typical use case, other reference points include the start of the measure.
Parameters:
-
starts(list) –A list of numeric start times.
-
tatum(Union[Fraction, int, None], default:None) –The tatum duration to use as the grid unit and values are rounded to it. If None, it is deduced automatically via
get_tatum_from_priorities.
Examples:
>>> starts_to_indices([0, 1/2, 2/3, 2.5])
[0, 3, 4, 15]
>>> starts_to_indices([0, 1/2, 2/3, 2.5], tatum=Fraction(1, 6))
[0, 3, 4, 15]
>>> starts_to_indices([0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 6.5, 6.6640625, 6.83203125, 7.0])
[0, 6, 12, 18, 24, 30, 36, 39, 40, 41, 42]
>>> starts_to_indices([0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 6.5, 6.6640625, 6.83203125, 7.0], tatum=Fraction(1, 6))
[0, 6, 12, 18, 24, 30, 36, 39, 40, 41, 42]
Also accepts tatum values greater than 1:
>>> starts_to_indices([3, 6, 9], tatum=3)
[1, 2, 3]
>>> starts_to_indices([3, 6, 9])
[1, 2, 3]
>>> starts_to_indices([0, 4, 8, 12, 16, 20, 24, 28, 32, 36, 40])
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
>>> starts_to_indices([28.0, 29.0, 29.5, 30.0, 32.0, 33.0, 34.0, 36.0, 38.0, 40.0])
[56, 58, 59, 60, 64, 66, 68, 72, 76, 80]
Source code in amads/time/meter/tatum.py
89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 | |