Rhythm construction#
biotuner.rhythm_construction — rhythm and polyrhythm construction from scales.
Module type: Functions
Euclidean rhythms, discrete polyrhythms (LCM grids), continuous-time polyrhythms (irrational ratios), second-order recursive polyrhythms, evolving rhythm processes (phase shift, metric modulation, density ramps, onset interpolation), MIDI / OSC export, and rich visualizations.
- scale2euclid(scale, max_denom=10, mode='normal')[source]#
Generate Euclidean rhythms based on a list of scale factors.
- Parameters:
scale (List of float) – A list of positive floats representing the scale steps.
max_denom (int, default=10) – An integer representing the maximum denominator allowed for the fractions generated by scale2frac().
mode (str, default=’normal’) – A string representing the mode of Euclidean rhythms to generate. Mode options:
‘normal’ : generate rhythms with num steps distributed over “denom” beats. ‘full’ : generate rhythms with all possible combinations
of num and denom for a given scale.
- Returns:
euclid_patterns (List of lists) – A list of lists containing the Euclidean rhythms generated by the function.
- Raises:
TypeError – If scale is not a list of positive floats, or max_denom is not an integer.
ValueError – If max_denom is not a positive integer.
Notes
The scale parameter is first converted into fractions using the scale2frac() function. Euclidean rhythms are then generated based on the num and denom values of the fractions, using the bjorklund() function. If mode is set to “normal”, the function generates one rhythm for each fraction where denom is less than or equal to max_denom. If mode is set to “full”, the function generates all possible combinations of num and denom for each fraction where denom is less than or equal to max_denom.
Examples
>>> scale2euclid([1.33, 1.5, 1.75], max_denom=8, mode="normal") [[1, 1, 1, 0], [1, 1, 0], [1, 0, 1, 0, 1, 0, 1]]
>>> scale2euclid([1.33, 1.5, 1.75], max_denom=8, mode="full") [[1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0], [1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0], [1, 0, 1, 0, 1, 0], [1, 0, 0, 1, 0, 0], [1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0], [1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0]]
- invert_ratio(ratio: float, n_steps_down: int, limit_denom: int = 64)[source]#
Inverts a given ratio by dividing 1 by it, then dividing the result by the ratio n_steps_down times.
- Parameters:
ratio (float) – The ratio to be inverted.
n_steps_down (int) – The number of times to divide the ratio.
limit_denom (int, default=64) – The maximum denominator of the fraction returned.
- Returns:
Tuple [sp.Rational, float] –
The resulting fraction after dividing 1 by the ratio and then dividing it n_steps_down times.
The resulting float after dividing 1 by the ratio and then dividing it n_steps_down times.
- binome2euclid(binome, n_steps_down=1, limit_denom=64)[source]#
Convert a set of two ratios to Euclidean rhythms.
- Parameters:
binome (list of two floats) – The two ratios to be converted.
n_steps_down (int, default=1) – The number of times to apply the inversion operator.
limit_denom (int, default=64) – The maximum denominator to be used when approximating fractions.
- Returns:
tuple –
the generated Euclidean rhythms
the inverted ratios
the approximated fractions.
Notes
The Euclidean rhythms are generated using the bjorklund algorithm, as implemented in the bjorklund function. The input ratios are inverted according to the number of steps specified by the n_steps_down parameter. The resulting ratios are then used to generate a pair of approximated fractions, which are combined to produce the final Euclidean rhythms.
- consonant_euclid(scale, n_steps_down=2, limit_denom=64, limit_cons=0.1, limit_denom_final=16)[source]#
Computes Euclidean rhythms and consonant steps between them based on a given scale.
- Parameters:
scale (list of floats) – Musical scale.
n_steps_down (int, default=2) – The number of steps the Euclidean rhythms is shifted down.
limit_denom (int, default=64) – The upper bound of the denominator of the resulting fractions.
limit_cons (float, default=0.1) – The lower bound of the consonance measure.
limit_denom_final (int, default=16) – The upper bound of the denominator of the final consonant steps.
- Returns:
euclid_final, cons_step ((list[list], list[int])) – A tuple containing the following elements: - List of lists representing the Euclidean rhythms that satisfy the given consonance measure and denominator bounds. - List of integers representing the consonant steps between the Euclidean rhythms.
- interval_vector(euclid)[source]#
Computes the interval vector for a given Euclidean rhythm.
- Parameters:
euclid (array_like) – A binary array representing a Euclidean rhythm. Must have at least one pulse.
- Returns:
Interval_vector (numpy.ndarray) – An interval vector as a numpy array.
Examples
>>> euclid = [1, 1, 0, 1, 1, 0, 1] >>> interval_vector(euclid) array([1, 2, 1, 2, 1])
>>> euclid = [1, 0, 0, 1, 0, 0, 1] >>> interval_vector(euclid) array([3, 3, 1])
- bjorklund(steps, pulses)[source]#
Generate a Euclidean rhythm pattern using Bjorklund’s algorithm. From brianhouse/bjorklund
- Parameters:
steps (int) – The number of steps in the pattern.
pulses (int) – The number of pulses in the pattern.
- Returns:
pattern (numpy.ndarray) – A binary Euclidean rhythm pattern with the specified number of steps and pulses.
- Raises:
ValueError – If pulses is greater than steps.
Examples
Generate a Euclidean rhythm pattern with 16 steps and 5 pulses:
>>> bjorklund(16, 5) array([1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0])
- interval_vec_to_string(interval_vectors)[source]#
Convert a list of interval vectors to a list of string representations.
- Parameters:
interval_vectors (list of array_like) – A list of interval vectors, where each interval vector is an array_like object representing the number of steps between each hit.
- Returns:
strings (list of str) – A list of string representations of the interval vectors, where each string is of the form ‘E(n,k)’, where n is the number of hits and k is the number of steps.
Examples
>>> interval_vectors = [[2, 2, 2, 2, 1], [3, 3, 1]] >>> interval_vec_to_string(interval_vectors) ['E(5,9)', 'E(3,7)']
- euclid_string_to_referent(strings, dict_rhythms)[source]#
This function takes a list of strings of Euclidean rhythms represented in the E(n,k) format and returns a list of their referents in the given dictionary of rhythms.
- Parameters:
strings (list of str) – A list of Euclidean rhythms represented as E(n,k) strings.
dict_rhythms (dict) – A dictionary of rhythms where keys are the E(n,k) representation and values are the referents.
- Returns:
referent (list of str) – A list of the referents of the input rhythms. “None” is used for rhythms not found in the dictionary.
Examples
>>> from biotuner.dictionaries import dict_rhythms >>> strings = ["E(5,9)", "E(9,16)"] >>> euclid_string_to_referent(strings, dict_rhythms) ['It is a popular Arabic rhythm called Agsag-Samai. Started on the second onset, it is a drum pattern used by the Venda in South Africa, as well as a Rumanian folk-dance rhythm. It is also the rhythmic pattern of the Sigaktistos rhythm of Greece, and the Samai aktsak rhythm of Turkey. Started on the third onset, it is the rhythmic pattern of the Nawahiid rhythm of Turkey.', 'It is a rhythm necklace used in the Central African Republic. When it is started on the second onset it is a bell pattern of the Luba people of Congo. When it is started on the fourth onset it is a rhythm played in West and Central Africa, as well as a cow-bell pattern in the Brazilian samba. When it is started on the penultimate onset it is the bell pattern of the Ngbaka-Maibo rhythms of the Central African Republic.']
- euclid_long_to_short(pattern)[source]#
Converts a long Euclidean rhythm pattern to a short representation.
- Parameters:
pattern (list of int) – The long Euclidean rhythm pattern, represented as a list of integers where 1 represents a hit and 0 represents a rest.
- Returns:
list of int – A list with two integers representing the short representation of the rhythm. The first integer represents the number of hits in the rhythm and the second integer represents the total number of steps.
Examples
>>> euclid_long_to_short([1, 0, 1, 0, 1, 0, 1]) [3, 7]
- find_optimal_offsets(pulses_steps)[source]#
Finds the optimal offset values for a set of Euclidean rhythms that aligns multiple pulses from different rhythms together. Args:
pulses_steps (List[Tuple[int,int]]): A list of tuples, where each tuple represent the number of pulses and steps of a rhythm.
- Returns:
List[int]: A list of optimal offset values for the rhythms in pulses_steps
- euclidean_rhythm(pulses, steps, offset=0)[source]#
Generate a Euclidean rhythm. Args:
pulses (int): The number of pulses in the rhythm. steps (int): The number of steps in the rhythm. offset (int): An offset for the rhythm in pulses.
- Returns:
List[int]: A binary list representing the rhythm, where 1 indicates a pulse and 0 indicates no pulse.
- visualize_rhythms(pulses_steps, offsets=None, plot_size=6, tolerance=0.1, cmap='Set3')[source]#
Visualize multiple Euclidean rhythms.
- Parameters:
pulses_steps (list of tuple) – A list of tuple, where each tuple represent the number of pulses and steps of a rhythm.
offsets (list of int, optional) – A list of offsets for each rhythm in pulses_steps.
plot_size (int, optional) – The size of the plot.
tolerance (float, optional) – The tolerance for considering two pulses to be in the same rhythm.
- Returns:
None
- scale2polyrhythm(scale, max_denom=16)[source]#
Build a polyrhythmic grid from a list of scale ratios.
Each ratio
p/qcontributes one voice:ppulses distributed overqsteps using Bjorklund’s algorithm. All voices are then tiled to the LCM of every step-count so they share a common timeline.- Parameters:
scale (list of float) – Frequency ratios (e.g. peaks from biotuner analysis).
max_denom (int, default=16) – Maximum denominator when approximating each ratio as a fraction.
- Returns:
voices (list of list of int) – Binary onset patterns, all of length
lcm_steps.coincidences (numpy.ndarray of int) – Number of simultaneous onsets at each position in the LCM grid.
coincidence_positions (list of int) – Zero-indexed positions where 2 or more voices sound together.
labels (list of str) – Voice label, e.g.
"E(3,4)".lcm_steps (int) – Length of the common grid.
Examples
>>> voices, coinc, coinc_pos, labels, L = scale2polyrhythm([1.333, 1.5, 1.75]) >>> labels ['E(3,4)', 'E(2,3)', 'E(3,4)']
- plot_polyrhythm_piano_roll(scale, max_denom=16, cmap='Set2', figsize=None, title='Polyrhythm Grid – Piano Roll')[source]#
Visualize a polyrhythmic grid as a piano roll.
Each row is one rhythmic voice derived from the scale ratios. Filled cells mark onsets; columns where two or more voices coincide are highlighted with a translucent orange bar.
- Parameters:
scale (list of float) – Frequency ratios passed to
scale2polyrhythm().max_denom (int, default=16) – Maximum denominator for fraction approximation.
cmap (str, default=”Set2”) – Seaborn / Matplotlib colour palette name for voice colours.
figsize (tuple or None) – Figure size. Defaults to
(lcm_steps * 0.45, n_voices * 0.9).title (str) – Plot title.
- Returns:
fig, ax (matplotlib Figure and Axes)
- plot_polyrhythm_coincidence(scale, max_denom=16, cmap='Set2', figsize=(10, 3), title='Polyrhythm Grid – Coincidence Density')[source]#
Plot the number of simultaneous onsets across all rhythmic voices over time.
The filled area shows rhythmic tension: peaks mark structural downbeats where multiple voices land together; troughs are moments of dispersion. Individual voice onset markers are drawn at the bottom.
- Parameters:
scale (list of float) – Frequency ratios passed to
scale2polyrhythm().max_denom (int, default=16) – Maximum denominator for fraction approximation.
cmap (str, default=”Set2”) – Colour palette for individual voice rug marks.
figsize (tuple, default=(10, 3)) – Figure size.
title (str) – Plot title.
- Returns:
fig, ax (matplotlib Figure and Axes)
- plot_polyrhythm_coincidence_matrix(scale, max_denom=16, cmap='YlOrRd', figsize=None, title='Polyrhythm Grid – Coincidence Matrix')[source]#
Heatmap of pairwise coincidences between rhythmic voices.
matrix[i][j]counts how many steps in the LCM grid have onsets in both voice i and voice j. The diagonal gives each voice’s total onset count. Highly consonant ratio pairs cluster in the upper-left corner after sorting by onset density.- Parameters:
scale (list of float)
max_denom (int, default=16)
cmap (str, default=”YlOrRd”)
figsize (tuple or None)
title (str)
- Returns:
fig, ax (matplotlib Figure and Axes)
matrix (numpy.ndarray) – The raw coincidence matrix (n_voices × n_voices).
- polyrhythm_to_midi(scale, bpm=120, steps_per_bar=16, subdivisions=None, notes=None, velocity=100, max_denom=16, output_path='polyrhythm.mid', n_bars=1)[source]#
Export a polyrhythmic grid as a multi-track MIDI file.
Each rhythmic voice is a separate MIDI track on GM drum channel 9.
- Parameters:
scale (list of float)
bpm (float, default=120)
steps_per_bar (int, default=16) – Global fallback subdivision when
subdivisionsisNone.subdivisions (int | str | list | None) – Per-voice grid resolution. See
SUBDIV_SIXTEENTHand siblings for named constants, or use'8th','16th','quarter'etc. A single value is broadcast to all voices; a list assigns one per voice. Examples:subdivisions=SUBDIV_SIXTEENTH # all voices at 16th notes subdivisions=['16th', '8th', 'quarter'] # three different grids subdivisions=[16, 8, 4] # same, numeric
notes (list of int or None)
velocity (int, default=100)
max_denom (int, default=16)
output_path (str)
n_bars (int, default=1)
- Returns:
str — resolved output path.
- euclid_polyrhythm_to_midi(rhythms, bpm=120, notes=None, velocity=100, note_length=60, mode='bar', base_subdivision=16, output_path='euclid_polyrhythm.mid', n_bars=4)[source]#
Export Euclidean polyrhythms as MIDI, with each voice on its own independent grid.
Two timing modes control when voices realign:
mode='bar'(default — shared bar)Every voice divides the same bar into
stepsequal slices. All voices restart at every bar boundary → realign every 1 bar. Use this to hear how different Euclidean patterns feel within the same metrical unit.mode='free'(independent cycles)All voices share the same base step size (
bar_ticks / base_subdivision), so each voice’s cycle is proportional to itsstepscount. Voices only realign at LCM(all steps) base-steps — potentially many bars away. Use this for true polyrhythmic drift:E(3,8) + E(4,12) + E(5,7) → LCM(8,12,7)=168 steps at base_subdivision=16 → 168/16 = 10.5 bars before realignment
- Parameters:
rhythms (list of (int, int)) – Each tuple is
(pulses, steps)— the two parameters of a Euclidean rhythm. Examples:[(3, 8), (4, 12), (5, 7)] # three independent grids [(3, 4), (2, 3)] # 3-against-4 over 2-against-3
bpm (float, default=120)
notes (list of int or None) – MIDI note per voice. Defaults to the GM drum map.
velocity (int, default=100)
note_length (int, default=60) – Duration of each hit in MIDI ticks (480 PPQ). 60 ticks ≈ 62 ms at 120 BPM. Automatically clamped below the step duration.
mode ({‘bar’, ‘free’}, default=’bar’) – Timing model — see above.
base_subdivision (int, default=16) – Only used in
mode='free'. Number of equal divisions per bar that defines the shared step size. 16 = 16th notes, 8 = 8th notes, etc.output_path (str)
n_bars (int, default=4) – Number of bars to write. In
'free'mode the actual output length may differ slightly because voices are tiled until the total exceedsn_barsbars.
- Returns:
str — resolved output path.
Examples
>>> # shared bar — all voices restart every bar >>> euclid_polyrhythm_to_midi([(3,8),(4,12),(5,7)], mode='bar', ... n_bars=8, output_path="euclid_bar.mid") >>> # free cycles — realign only every 10.5 bars >>> euclid_polyrhythm_to_midi([(3,8),(4,12),(5,7)], mode='free', ... n_bars=16, output_path="euclid_free.mid")
- polyrhythm_to_osc_score(scale, bpm=120, steps_per_bar=16, max_denom=16, host='127.0.0.1', port=9000, base_address='/polyrhythm')[source]#
Build a timed OSC score from a polyrhythmic grid.
Returns a list of
(time_sec, address, args)tuples — one entry per onset across all voices. This is the offline score representation; usepolyrhythm_send_osc()to play it in real-time.OSC addresses emitted:
{base}/voice/{i}– onset on voice i (args: step, beat_pos_float){base}/coincidence– simultaneous onset on 2+ voices (args: step, n_voices)
- Parameters:
scale (list of float)
bpm (float, default=120)
steps_per_bar (int, default=16)
max_denom (int, default=16)
host (str, default=”127.0.0.1” (informational, stored in returned dict))
port (int, default=9000)
base_address (str, default=”/polyrhythm”)
- Returns:
score (list of tuple) – Sorted list of
(time_sec, address, args_list).meta (dict) –
{"host", "port", "bpm", "lcm_steps", "labels", "step_dur_sec"}.
- polyrhythm_send_osc(scale, bpm=120, steps_per_bar=16, max_denom=16, host='127.0.0.1', port=9000, base_address='/polyrhythm', n_loops=1)[source]#
Play a polyrhythmic grid in real-time by sending OSC messages.
Iterates through the LCM grid step-by-step at the given tempo, sending
/polyrhythm/voice/{i}messages for every onset. Blocks untiln_loopscycles complete (use a thread for background use).Targets: Ableton Live + Max4Live
[udpreceive], SuperCollider, VCV Rack, TouchDesigner, Pure Data, etc.- Parameters:
scale (list of float)
bpm (float, default=120)
steps_per_bar (int, default=16)
max_denom (int, default=16)
host (str, default=”127.0.0.1”)
port (int, default=9000)
base_address (str, default=”/polyrhythm”)
n_loops (int, default=1) – Number of times to loop the pattern.
- Returns:
None
- polyrhythm_dump_osc_patterns(scale, max_denom=16, host='127.0.0.1', port=9000, base_address='/polyrhythm')[source]#
Send the full binary patterns to an OSC receiver in one shot.
Fires a single OSC message per voice containing the complete binary pattern as a list of ints. Useful for syncing pattern state to a receiver (e.g. a Max4Live device or a custom step-sequencer) without real-time playback.
Addresses emitted:
{base}/voice/{i}/pattern– full binary list, length = LCM steps{base}/meta/labels– voice labels as a string{base}/meta/lcm– LCM step count (int)
- Parameters:
scale (list of float)
max_denom (int, default=16)
host (str, default=”127.0.0.1”)
port (int, default=9000)
base_address (str, default=”/polyrhythm”)
- scale2polyrhythm_iso(scale, max_denom=16)[source]#
Build an isochronous (classically spaced) polyrhythm from scale ratios.
Unlike the Bjorklund approach, each ratio
p/qcontributes two voices: one voice ofpevenly-spaced pulses and one ofqevenly-spaced pulses, both overlcm(p, q)steps. This is the classical drummer’s polyrhythm: 3-against-2, 4-against-3, etc.- Parameters:
scale (list of float)
max_denom (int, default=16)
- Returns:
voices, coincidences, coincidence_positions, labels, lcm_steps – Same signature as
scale2polyrhythm().
Examples
>>> voices, _, _, labels, L = scale2polyrhythm_iso([3/2]) >>> labels # 3 against 2 over LCM=6 ['iso(2,6)', 'iso(3,6)']
- scale2polyrhythm_harmonic(scale, n_harmonics=4, lcm_cap=64)[source]#
Place onsets at harmonic series positions derived from each ratio.
For ratio
r, thek-th onset is placed at positionfloor(k / r * lcm_steps)— mapping the integer harmonic series ofrinto a discrete time grid. This produces inharmonic polyrhythms whenris irrational, and interlocking regular patterns whenris simple.- Parameters:
scale (list of float) – Frequency ratios.
n_harmonics (int, default=4) – Number of harmonics per ratio voice.
lcm_cap (int, default=64) – Upper bound for the LCM grid (prevents explosion for irrational ratios).
- Returns:
voices, coincidences, coincidence_positions, labels, lcm_steps
- evolve_phase_shift(pattern, n_cycles=8, shift_per_cycle=1)[source]#
Reich-style phase shifting between a pattern and a shifted copy.
A fixed
pattern(voice 0) is held constant while a shifted copy (voice 1) advances byshift_per_cyclesteps each cycle. Returns a 3-D array(n_cycles, 2, len(pattern))and a 2-D coincidence matrix(n_cycles, len(pattern))counting overlaps at each step.- Parameters:
pattern (list of int) – Binary onset pattern (one cycle).
n_cycles (int, default=8) – Number of evolution steps to generate.
shift_per_cycle (int, default=1) – How many steps the second voice advances each cycle.
- Returns:
evolution (numpy.ndarray, shape (n_cycles, 2, L)) –
evolution[c, 0]= fixed voice at cycle c,evolution[c, 1]= shifted copy at cycle c.coincidences (numpy.ndarray, shape (n_cycles, L)) – Simultaneous onsets per step per cycle.
Examples
>>> evo, coinc = evolve_phase_shift([1,0,1,0,1,0,1,0], n_cycles=8) >>> evo.shape (8, 2, 8)
- evolve_metric_modulation(scale, n_cycles=6, bpm_start=120)[source]#
Build a metric modulation chain from a list of ratios.
Each ratio in
scaledefines a tempo multiplier applied to the current BPM. The pattern at each cycle uses the Bjorklund algorithm for the current ratio, scaled to a common reference grid ofref_stepssteps.- Returns:
patterns (list of list) – Binary pattern per cycle (length =
ref_steps).bpms (list of float) – Tempo at each cycle.
labels (list of str)
- evolve_onset_interpolation(pattern_a, pattern_b, n_frames=8)[source]#
Gradually morph onset positions from
pattern_atopattern_b.Onset positions in
pattern_aare linearly moved toward the nearest target position inpattern_bovern_framesframes. At frame 0 the result equalspattern_a; at framen_frames - 1it equalspattern_b. Intermediate frames occupy fractional positions, rounded to the nearest grid step.Both patterns must have the same length. If onset counts differ, the shorter is padded with positions from the longer.
- Parameters:
pattern_a (list of int)
pattern_b (list of int)
n_frames (int, default=8)
- Returns:
frames (numpy.ndarray, shape (n_frames, L)) – Binary onset matrix per frame.
- evolve_density_ramp(pattern, n_cycles=8, direction='grow')[source]#
Progressively add or remove onsets from a pattern over cycles.
Onsets are added (
direction='grow') or removed (direction='shrink') one-by-one in order of their position in the pattern, producing a ramp from silence to the full pattern or vice versa.- Parameters:
pattern (list of int)
n_cycles (int, default=8)
direction ({“grow”, “shrink”})
- Returns:
frames (numpy.ndarray, shape (n_cycles, len(pattern)))
- plot_rhythm_evolution(frames, labels=None, title='Rhythm Evolution', cmap='Blues', figsize=None)[source]#
Visualize an evolving rhythm as a time-unrolled piano roll.
Rows = cycles / frames (time flows downward), columns = grid steps. Each filled cell marks an onset; intensity can reflect coincidence count.
- Parameters:
frames (numpy.ndarray, shape (n_cycles, L)) – Binary or integer matrix. Each row is one cycle / frame.
labels (list of str or None) – Row labels (cycle names).
title (str)
cmap (str, default=”Blues”)
figsize (tuple or None)
- Returns:
fig, ax (matplotlib Figure and Axes)
- plot_phase_shift_evolution(pattern, n_cycles=8, shift_per_cycle=1, cmap='Set2', figsize=None, title='Phase Shift Evolution (Reich process)')[source]#
Visualize a Reich-style phase shift as two side-by-side evolution grids plus a third panel showing coincidence count per step per cycle.
- Parameters:
pattern (list of int)
n_cycles (int, default=8)
shift_per_cycle (int, default=1)
cmap (str, default=”Set2”)
figsize (tuple or None)
title (str)
- Returns:
fig, axes (matplotlib Figure and array of 3 Axes)
- scale2polyrhythm_continuous(scale, duration=1.0, max_denom=16)[source]#
Derive real polyrhythmic voices as continuous onset times in seconds.
For each ratio
p/qinscale, two independent isochronous streams are created:pequally-spaced onsets andqequally-spaced onsets, both spanning[0, duration). Duplicate pulse-counts across ratios are merged so the returned list contains unique voices only.Unlike the LCM-grid approach, onset times are stored as floats — there is no shared discrete grid. A 5:7 polyrhythm is genuinely 5 against 7, not a 35-step grid approximation.
- Parameters:
scale (list of float) – Frequency ratios (e.g. peak frequencies from biotuner).
duration (float, default=1.0) – Duration of one cycle in seconds.
max_denom (int, default=16) – Maximum denominator when approximating each ratio as a fraction.
- Returns:
onset_times (list of list of float) –
onset_times[i]= sorted onset positions (seconds) for voicei.labels (list of str) – Human-readable label per voice, e.g.
"3-pulse (0.33 s)".pulse_counts (list of int) – Number of pulses per voice (sorted ascending).
duration (float) – The cycle duration (echoed for convenience).
Examples
>>> times, labels, counts, dur = scale2polyrhythm_continuous([3/2], duration=2.0) >>> counts # 2-against-3 [2, 3] >>> times[0] # 2 pulses over 2 s [0.0, 1.0] >>> times[1] # 3 pulses over 2 s [0.0, 0.667, 1.333]
- coincidences_continuous(onset_times, tolerance_sec=0.005)[source]#
Find near-simultaneous onsets across continuous-time voices.
- Parameters:
onset_times (list of list of float) – Per-voice onset positions in seconds.
tolerance_sec (float, default=0.005) – Maximum time gap (seconds) for two onsets to be considered coincident.
- Returns:
coinc (list of dict) – Each entry:
{"time": float, "voices": list of int}. Sorted by time.
- plot_polyrhythm_score(scale, duration=1.0, max_denom=16, cmap='Set2', figsize=(12, 4), tolerance_sec=0.005, title=None)[source]#
Score-style visualization of a continuous-time polyrhythm.
Each voice is drawn as a horizontal staff line. Onset ticks are stem marks (vertical lines) at exact time positions — no discrete grid. Vertical grey bars connect onsets that coincide within
tolerance_sec. The inter-onset interval (IOI) is annotated below each staff.This is the standard way to notate 2:3, 3:4, 5:7, etc. polyrhythms in contemporary music notation.
- Parameters:
scale (list of float)
duration (float, default=1.0)
max_denom (int, default=16)
cmap (str, default=”Set2”)
figsize (tuple, default=(12, 4))
tolerance_sec (float, default=0.005) – Onset gap to treat as a coincidence for alignment lines.
title (str or None)
- Returns:
fig, ax (matplotlib Figure and Axes)
- plot_polyrhythm_phase_wheel(scale, duration=1.0, max_denom=16, cmap='Set2', figsize=(5, 5), title=None)[source]#
Single shared clock-face view of a continuous-time polyrhythm.
All voices are drawn on the same circle — what varies is the angular spacing of each voice’s spokes. A 2-pulse stream places two spokes 180° apart; a 3-pulse stream places three spokes 120° apart; a 5-pulse stream places five spokes 72° apart. Where spokes overlap, voices coincide.
This is fundamentally different from the Euclidean concentric-ring wheel: there, each voice lives on its own ring sized to its LCM grid position. Here, the clock face is continuous time — a 2:3 polyrhythm and a 3:4 polyrhythm genuinely look different because the angular gaps between coincidences change.
- Parameters:
scale (list of float)
duration (float, default=1.0)
max_denom (int, default=16)
cmap (str, default=”Set2”)
figsize (tuple, default=(5, 5))
title (str or None)
- Returns:
fig, ax (matplotlib Figure and Axes)
- polyrhythm_continuous_to_midi(scale, duration_sec=2.0, bpm=120, notes=None, velocity=100, max_denom=16, output_path='polyrhythm_continuous.mid', n_bars=4)[source]#
Export a continuous-time polyrhythm to MIDI with exact delta-time resolution.
Onset times are stored as MIDI delta-ticks with 480 PPQ resolution, giving sub-millisecond accuracy for the inter-stream phase relationships. The result is a genuine polyrhythm clip — not a quantised step sequencer.
- Parameters:
scale (list of float)
duration_sec (float, default=2.0) – Duration of one cycle in seconds.
bpm (float, default=120)
notes (list of int or None) – GM note numbers per voice.
velocity (int, default=100)
max_denom (int, default=16)
output_path (str, default=”polyrhythm_continuous.mid”)
n_bars (int, default=4) – Number of cycles to write.
- Returns:
str — output path
- extract_coincidence_rhythm(onset_times, duration, tolerance_sec=None, min_voices=2)[source]#
Extract the meta-rhythm formed by multi-voice coincidences.
Given
Nisochronous streams, finds every moment where at leastmin_voicesstreams fire near-simultaneously and returns the resulting coincidence times plus their IOI structure.The inter-onset intervals of the coincidence sequence, normalised by their minimum, become a new
scalethat can be fed into a second-order polyrhythm analysis.- Parameters:
onset_times (list of list of float)
duration (float)
tolerance_sec (float or None (default 1 % of duration))
min_voices (int, default 2)
- Returns:
coinc_times (list of float)
coinc_voices (list of list of int)
iois (numpy.ndarray (empty if < 2 coincidences))
ioi_scale (list of float (IOI ratios > 1, deduplicated; empty if trivial))
voice_count (list of int)
- second_order_polyrhythm(scale, duration=2.0, max_denom=16, tolerance_sec=None, min_voices=2, n_orders=2)[source]#
Build a hierarchy of polyrhythms where each level’s coincidences seed the next.
Algorithm#
Build the 1st-order polyrhythm from
scale.Find multi-voice coincidences → extract the meta-rhythm.
Derive IOI ratios from the meta-rhythm.
Repeat from step 1 using the IOI ratios as the new scale, up to
n_orderstotal levels.
- Parameters:
scale (list of float)
duration (float, default 2.0)
max_denom (int, default 16)
tolerance_sec (float or None)
min_voices (int, default 2)
n_orders (int, default 2)
- returns:
orders (list of dict) – Keys per entry:
"order","scale","onset_times","labels","pulse_counts","coinc_times","coinc_voices","voice_count","iois","ioi_scale","n_coinc".
- plot_second_order_polyrhythm(scale, duration=2.0, max_denom=16, tolerance_sec=None, min_voices=2, n_orders=2, cmap='Set2', figsize=None, title=None)[source]#
Multi-row score showing each order of a polyrhythm hierarchy.
For every level the plot shows: * A voice-score row — all streams with stem marks at exact float positions. * A coincidence row — the meta-rhythm extracted from that level, coloured
by multiplicity (how many voices fire together), with IOI values annotated and the derived 2nd-order scale printed on the x-axis.
- Parameters:
scale (list of float)
duration (float, default 2.0)
max_denom (int, default 16)
tolerance_sec (float or None)
min_voices (int, default 2)
n_orders (int, default 2)
cmap (str, default “Set2”)
figsize (tuple or None)
title (str or None)
- Returns:
fig (matplotlib Figure)
axes (list of matplotlib Axes)
orders (list of dict (raw data from
second_order_polyrhythm))
- voices_to_midi(voices, labels, lcm_steps, bpm=120, steps_per_bar=16, subdivisions=None, notes=None, velocity=100, output_path='voices.mid', n_bars=4)[source]#
Generic MIDI exporter for any list of binary rhythmic voices.
Accepts pre-computed
(voices, labels, lcm_steps)as returned byscale2polyrhythm(),scale2polyrhythm_iso(), orscale2polyrhythm_harmonic(). Each voice becomes one MIDI track on GM drum channel 9.- Parameters:
voices (list of list of int) – Binary onset patterns, each of length
lcm_steps.labels (list of str)
lcm_steps (int)
bpm (float, default=120)
steps_per_bar (int, default=16) – Global fallback when
subdivisionsisNone.subdivisions (int | str | list | None) – Per-voice grid resolution. Controls how densely each voice reads its pattern in real time. Use named constants or strings:
voices_to_midi(v, l, L, subdivisions=[16, 8, 4]) # voice 0 → 16th notes, voice 1 → 8th notes, voice 2 → quarter notes voices_to_midi(v, l, L, subdivisions=['16th', '8th', 't_8th']) # voice 0 → 16ths, voice 1 → 8ths, voice 2 → 8th-note triplets
A single value (int or str) is broadcast to all voices. Valid string aliases:
'whole' 'half' 'quarter' '8th' '16th' '32nd' 't_quarter' 't_8th' 't_16th'.notes (list of int or None)
velocity (int, default=100)
output_path (str)
n_bars (int, default=4)
- Returns:
str — resolved output path.
Examples
>>> voices, _, _, labels, L = scale2polyrhythm_iso([3/2]) >>> # all voices at 16th-note grid >>> voices_to_midi(voices, labels, L, bpm=90, output_path="iso.mid") >>> # voice 0 at 16ths, voice 1 at 8ths, rest at quarter notes >>> voices_to_midi(voices, labels, L, bpm=90, ... subdivisions=[16, 8, 4, 4, 4], ... output_path="iso_mixed.mid")
- frames_to_midi(frames, bpm=120, steps_per_bar=None, subdivisions=None, notes=None, velocity=100, output_path='evolution.mid', voice_labels=None, mode='clips')[source]#
Export an evolving rhythm sequence to MIDI.
Two modes control how the evolution is arranged in the file:
mode='clips'(default)Each frame gets its own MIDI track, all starting at t=0. Every track is exactly one bar long. In a DAW (Ableton, Logic, …) you get
n_framesindependent one-bar clips you can trigger, layer, or mute individually — ideal for live performance or A/B comparison. Each frame is assigned a distinct drum note so they sound different.mode='sequence'The classic linear layout: one track per voice, with all frames played back-to-back as consecutive bars. Useful for hearing the full evolution arc in one pass.
Input shapes#
2-D
(n_frames, L)— one evolving voice.3-D
(n_frames, n_voices, L)— several voices per frame (e.g.evolve_phase_shift()returns(n_cycles, 2, L)). In'clips'mode each frame gets one track that contains all voices at different pitches; in'sequence'mode each voice gets its own long track.
- Parameters:
frames (array-like, shape (n_frames, L) or (n_frames, n_voices, L))
bpm (float, default=120)
steps_per_bar (int or None) – Grid steps per bar. Defaults to L.
notes (list of int or None) – Base GM note list. In
'clips'mode each frame shifts up the list.velocity (int, default=100)
output_path (str)
voice_labels (list of str or None)
mode ({‘clips’, ‘sequence’}, default=’clips’)
- returns:
str — resolved output path.
Examples
>>> evo, _ = evolve_phase_shift([1,0,1,0,1,0,1,0], n_cycles=8) >>> frames_to_midi(evo, bpm=100, output_path="phase_shift.mid") # 8 clip tracks 'phase_shift.mid'
>>> frames = evolve_onset_interpolation(pattern_a, pattern_b, n_frames=8) >>> frames_to_midi(frames, bpm=120, mode='sequence', ... output_path="interpolation_seq.mid") # linear arc 'interpolation_seq.mid'
- metric_modulation_to_midi(patterns, bpms, labels, bpm=120, steps_per_bar=16, notes=None, velocity=100, output_path='metric_modulation.mid', tempo_automation=False)[source]#
Export a metric modulation sequence to MIDI.
Accepts the output of
evolve_metric_modulation().- Default behaviour (
tempo_automation=False) Each cycle pattern becomes its own MIDI track, all at a single fixed BPM. The file does not contain any tempo-change events, so loading it into a DAW will not alter your project tempo. Each track is one bar long and can be triggered or layered independently. The BPM ratio encoded in each pattern is expressed through the rhythm itself (denser/sparser hits), not through clock speed.
- Automation mode (
tempo_automation=True) All cycles are merged into a single track with a MIDI
set_tempoevent at the start of every bar. Use this only when you intentionally want the MIDI file to drive the host transport tempo.
- Parameters:
patterns (list of list of int)
bpms (list of float) – Tempo at each cycle (informational in default mode; structural in automation mode).
labels (list of str)
bpm (float, default=120) – Fixed project BPM used when
tempo_automation=False.steps_per_bar (int, default=16)
notes (list of int or None) – One GM note per cycle track. Default: cycles through the GM drum map.
velocity (int, default=100)
output_path (str)
tempo_automation (bool, default=False) – When
True, inject per-bar tempo changes (old behaviour).
- Returns:
str — resolved output path.
Examples
>>> pats, bpms, labels = evolve_metric_modulation([3/2, 5/4], bpm_start=90) >>> metric_modulation_to_midi(pats, bpms, labels, bpm=90, ... output_path="modulation.mid") 'modulation.mid'
- Default behaviour (
- second_order_polyrhythm_to_midi(orders, bpm=120, duration_sec=2.0, notes=None, velocity=100, output_path='second_order.mid', n_bars=4, separate_files=False)[source]#
Export a 2nd-order polyrhythm hierarchy to MIDI.
Accepts the list of order dicts returned by
second_order_polyrhythm(). Two export modes are available:separate_files=False(default) — all levels stacked into one file. Order 1 voices occupy the firstktracks; order 2 voices follow. Each voice is a separate track on GM drum channel 9. Voices from different orders use different drum notes so they are audibly distinct.separate_files=True— one MIDI file per order level, named<stem>_order1.mid,<stem>_order2.mid, etc.
- Parameters:
orders (list of dict) – Output of
second_order_polyrhythm().bpm (float, default=120)
duration_sec (float, default=2.0) – Duration of one cycle in seconds.
notes (list of list of int or None) –
notes[i]= GM notes for orderi. Auto-assigned whenNone.velocity (int, default=100)
output_path (str)
n_bars (int, default=4)
separate_files (bool, default=False)
- Returns:
list of str — output path(s).
- beat_envelope(peaks, amplitudes=None, duration_s=1.0, sr=1000, *, return_signal=False)[source]#
Time-domain amplitude envelope of a multi-frequency superposition.
Builds the signal:
x(t) = Σ_i a_i · cos(2π · f_i · t)
sampled at
srHz overduration_sseconds, then returns its instantaneous amplitude envelope|x(t)|(or|x(t)|²via the standard Hilbert-transform analytic signal).For a 2-tone chord with close frequencies, the envelope shows the classical beat at
|f_1 − f_2|. For a multi-tone chord, the envelope is a more complex periodic function whose Fourier content is the chord’s pairwise-difference spectrum — the temporal counterpart to the spatial interference fields inbiotuner.harmonic_geometry.interference_patterns.- Parameters:
peaks (sequence of float) – Component frequencies (Hz).
amplitudes (sequence of float, optional) – Per-component amplitudes. Defaults to uniform.
duration_s (float, default=1.0) – Duration of the signal in seconds.
sr (int, default=1000) – Sampling rate (Hz).
return_signal (bool, default=False) – If True, also return the raw signal
x(t)alongside the envelope (useful for inspection / plotting).
- Returns:
t (numpy.ndarray) – Time axis (length
int(duration_s * sr)).envelope (numpy.ndarray) – Instantaneous amplitude envelope
|x_analytic(t)|.signal (numpy.ndarray, only if
return_signal=True) – The raw signalx(t).
Examples
>>> import numpy as np >>> t, env = beat_envelope([100.0, 102.0], duration_s=2.0, sr=2000) >>> # Beat period = 1 / |102 - 100| = 0.5 s >>> # Envelope minima should be ~0.5 s apart.
- beat_spectrogram(peaks, amplitudes=None, duration_s=2.0, sr=1000, *, n_fft=512, hop=128, window='hann', log_power=True)[source]#
Time-frequency representation of a chord’s beating signal.
Builds the multi-frequency superposition (see
beat_envelope()) and returns a short-time Fourier transform magnitude. The chord’s fundamental frequencies appear as horizontal bands; their intermodulation produces vertical “beat” stripes whose density encodes pairwise differences.- Parameters:
peaks (sequence of float)
amplitudes (sequence of float, optional)
duration_s (float, default=2.0)
sr (int, default=1000)
n_fft (int, default=512) – FFT length (in samples).
hop (int, default=128) – Step between successive FFT windows (in samples).
window (str, default=’hann’) – Window function name (passed to
scipy.signal.get_window).log_power (bool, default=True) – If True, return
log(1 + |S|²); otherwise return|S|.
- Returns:
t (numpy.ndarray) – Time axis (frame centres, length = number of STFT frames).
f (numpy.ndarray) – Frequency axis (length
n_fft // 2 + 1).S (numpy.ndarray) – Spectrogram of shape
(len(f), len(t)).