subsequence.tuning

Microtonal tuning support for Subsequence.

Provides the Tuning class for specifying alternative tuning systems, a parser for Scala .scl files, and apply_tuning_to_pattern() which injects per-note pitch bend events so that any MIDI-compatible synthesiser can play the tuning without MPE or special hardware support.

Pitch bend is injected automatically:

  • Monophonic patterns (no overlapping notes): a single pitch bend event precedes each note on the pattern’s own channel.

  • Polyphonic patterns (overlapping notes): notes are spread across an explicit channel pool via ChannelAllocator. Each channel receives an independent pitch bend, so simultaneous notes can carry different tuning offsets. The channel pool must be supplied by the caller.

Typical usage via Composition.tuning() (applies globally, automatically):

comp.tuning(“meanquar.scl”, bend_range=2.0)

Per-pattern override via the PatternBuilder.apply_tuning() post-build transform:

p.apply_tuning(Tuning.equal(19), bend_range=2.0)

Module Contents

class subsequence.tuning.ChannelAllocator(channels: List[int])[source]

Assign MIDI channels from a pool for polyphonic channel rotation.

Tracks which channels are busy (a note is sounding) and which are free. Channels are reclaimed once a note ends (pulse ≥ release_pulse).

A simple round-robin fallback is used when all channels are busy (simultaneous voices exceed pool size) — accompanied by a warning log.

allocate(pulse: int, duration: int) int[source]

Return a free channel for a note starting at pulse lasting duration pulses.

class subsequence.tuning.Tuning[source]

A microtonal tuning system expressed as cent offsets from the unison.

The cents list contains the cent values for scale degrees 1 through N. Degree 0 (the unison, 0.0 cents) is always implicit and not stored. The last entry is typically 1200.0 cents (the octave) for octave-repeating scales, but any period is supported.

Create a Tuning from a file or programmatically:

Tuning.from_scl(“meanquar.scl”) # Scala .scl file Tuning.from_cents([100, 200, …, 1200]) # explicit cents Tuning.from_ratios([9/8, 5/4, …, 2]) # frequency ratios Tuning.equal(19) # 19-tone equal temperament

classmethod equal(divisions: int = 12, period: float = 1200.0) Tuning[source]

Construct an equal-tempered tuning with divisions equal steps per period.

Tuning.equal(12) is standard 12-TET (no pitch bend needed). Tuning.equal(19) gives 19-tone equal temperament.

classmethod from_cents(cents: List[float], description: str = '') Tuning[source]

Construct a tuning from a list of cent values for degrees 1..N.

The implicit degree 0 (unison, 0.0 cents) is not included in cents. The last value is typically 1200.0 for an octave-repeating scale.

classmethod from_ratios(ratios: List[float], description: str = '') Tuning[source]

Construct a tuning from frequency ratios relative to 1/1.

Each ratio is converted to cents via 1200 × log₂(ratio). Pass 2 or 2.0 for the octave (1200 cents).

classmethod from_scl(source: str | os.PathLike) Tuning[source]

Parse a Scala .scl file.

source is a file path. Lines beginning with ! are comments. The first non-comment line is the description. The second is the integer count of pitch values. Each subsequent line is a pitch:

  • Contains . → cents (float).

  • Contains / or is a bare integer → ratio; converted to cents via 1200 × log₂(ratio).

Raises ValueError for malformed files.

classmethod from_scl_string(text: str) Tuning[source]

Parse a Scala .scl file from a string (useful for testing).

pitch_bend_for_note(midi_note: int, reference_note: int = 60, bend_range: float = 2.0) Tuple[int, float][source]

Return (nearest_12tet_note, bend_normalized) for a MIDI note number.

The MIDI note number is interpreted as a scale degree relative to reference_note (default 60 = C4, degree 0 of the scale). The tuning’s cent table determines the exact frequency, and the nearest 12-TET MIDI note plus a fractional pitch bend corrects the remainder.

Parameters:
  • midi_note – The MIDI note to tune (0–127).

  • reference_note – MIDI note number that maps to degree 0 of the scale.

  • bend_range – Pitch wheel range in semitones (must match the synth’s pitch-bend range setting). Default ±2 semitones.

Returns:

A tuple (nearest_note, bend_normalized) where nearest_note is the integer MIDI note to send and bend_normalized is the normalised pitch bend value (-1.0 to +1.0).

property period_cents: float[source]

Cent span of one period (typically 1200.0 for octave-repeating scales).

property size: int[source]

Number of scale degrees per period (the .scl count line).

subsequence.tuning.apply_tuning_to_pattern(pattern: subsequence.pattern.Pattern, tuning: Tuning, bend_range: float = 2.0, channels: List[int] | None = None, reference_note: int = 60) None[source]

Apply a microtonal tuning to all notes in a pattern in place.

For each note:

  1. The nearest 12-TET MIDI note is computed and replaces note.pitch.

  2. A pitchwheel CcEvent is injected at the note’s onset with the fractional bend that corrects from the nearest 12-TET pitch to the exact tuned frequency.

  3. If channels is provided and the pattern has overlapping notes, notes are spread across the channel pool (ChannelAllocator).

Existing pitchwheel events (e.g., from p.portamento() or p.slide()) are shifted additively by the tuning offset of the note sounding at each pulse. Bend-reset-to-zero events are replaced with bend-reset-to-tuning-offset events.

Parameters:
  • pattern – The pattern to transform in place.

  • tuning – The Tuning object specifying cent offsets.

  • bend_range – Must match the MIDI synth’s pitch-bend range setting (default ±2 semitones).

  • channels – Optional explicit channel pool for polyphonic parts. When None, all notes stay on pattern.channel. Under polyphonic rotation, expression events created BEFORE this call (portamento()/slide() bends) are not re-routed per note — apply tuning last, or avoid combining note-correlated bends with a channel pool.

  • reference_note – MIDI note number mapped to scale degree 0.