subsequence
Subsequence - an algorithmic composition framework for Python.
Subsequence gives you a palette of mathematical building blocks - Euclidean rhythms, cellular automata, L-systems, Markov chains, cognitive melody generation - and a stateful engine that lets them interact and evolve over time. Unlike tools that loop a fixed pattern forever, Subsequence rebuilds every pattern fresh before each cycle with full context, so algorithms feed into each other and compositions emerge that no single technique could produce alone. It generates pure MIDI (no audio engine) to control hardware synths, modular systems, drum machines, or software VSTs/DAWs.
What makes it different:
- A rich algorithmic palette. Euclidean and Bresenham rhythm generators, cellular automata (1D and 2D), L-system string rewriting, Markov chains, cognitive melody via the Narmour model, probability- weighted ghost notes, position-aware thinning, drones and continuous notes, Perlin and pink noise, logistic chaos maps - plus groove templates, velocity shaping, and pitch-bend automation to shape how they sound.
- Stateful patterns that evolve. Each pattern is a Python function rebuilt fresh every cycle with full context - current chord, section, cycle count, shared data from other patterns. A Euclidean rhythm can thin itself as tension builds, a cellular automaton can seed from the harmony, and a Markov chain can shift behaviour between sections.
- Optional chord graph. Define weighted chord and key transitions via probability graphs, with gravity and automatic voice leading. Eleven built-in palettes and frozen progressions to lock some sections while others evolve freely. Layer on cognitive harmony for Narmour-based melodic inertia.
- Sub-microsecond clock. Hybrid sleep+spin timing achieves typical pulse jitter of < 5 us on Linux, with zero long-term drift.
- Turn anything into music.
composition.schedule()runs any Python function on a beat cycle - APIs, sensors, files. Anything Python can reach becomes a musical parameter. - Pure MIDI, zero sound engine. No audio synthesis, no heavyweight dependencies. Route to hardware synths, drum machines, Eurorack, or software instruments.
Composition tools:
- Rhythm and feel. Euclidean and Bresenham generators, multi-voice
weighted Bresenham distribution (
bresenham_poly()), ghost note layers (ghost_fill()), position-aware note removal (thin()- the musical inverse ofghost_fill), evolving cellular-automaton rhythms (cellular_1d(),cellular_2d()), smooth Perlin noise (perlin_1d(),perlin_2d(),perlin_1d_sequence(),perlin_2d_grid()), deterministic chaos sequences (logistic_map()), pink 1/f noise (pink_noise()), L-system string rewriting (p.lsystem()), Markov-chain generation (p.markov()), aperiodic binary rhythms (p.thue_morse()), golden-ratio beat placement (p.fibonacci()), Gray-Scott reaction-diffusion patterns (p.reaction_diffusion()), Lorenz strange-attractor generation (p.lorenz()), exhaustive pitch-subsequence melodies (p.de_bruijn()), step-wise melodies with guaranteed pitch diversity (p.self_avoiding_walk()), drones and explicit note on/off events (p.drone(),p.drone_off(),p.silence()), groove templates (Groove.swing(),Groove.from_agr()), swing viap.swing()(a shortcut forGroove.swing()), randomize, velocity shaping and ramps (p.velocity_ramp()), dropout, per-step probability, and polyrhythms via independent pattern lengths. - Melody generation.
p.melody()withMelodicStateapplies the Narmour Implication-Realization model to single-note lines: continuation after small steps, reversal after large leaps, chord-tone weighting, range gravity, and pitch-diversity penalty. History persists across bar rebuilds for natural phrase continuity. - Expression. CC messages/ramps, pitch bend, note-correlated bend/portamento/slide, program changes, SysEx, and OSC output - all from within patterns.
- Form and structure. Musical form as a weighted graph, ordered list,
or generator. Patterns read
p.sectionto adapt. Conductor signals (LFOs, ramps) shape intensity over time. - Mini-notation.
p.seq("x x [x x] x", pitch="kick")- concise string syntax for rhythms, subdivisions, and per-step probability. - Scales and quantization.
p.quantize()snaps notes to any scale.scale_notes()generates a list of MIDI note numbers from a key, mode, and range or note count - useful for arpeggios, Markov chains, and melodic walks. Built-in western and non-western modes, plusregister_scale()for your own. - Microtonal tuning.
composition.tuning()applies a tuning system globally;p.apply_tuning()overrides per-pattern. Supports Scala.sclfiles, explicit cent lists, frequency ratios, and N-TET equal temperaments. Polyphonic parts use explicit channel rotation so simultaneous notes can carry independent pitch bends without MPE. Compatible with any standard MIDI synthesiser. - Randomness tools. Weighted choice, no-repeat shuffle, random
walk, probability gates. Deterministic seeding (
seed=42) makes every decision repeatable. - Pattern transforms. Legato, staccato, reverse, double/half-time,
shift, transpose, invert, randomize, and conditional
p.every().
Integration:
- MIDI clock. Master (
clock_output()) or follower (clock_follow=True). When multiple inputs are connected, only one may be designated as the master clock source; messages from other inputs are filtered to prevent sync interference. Sync to a DAW or drive hardware. - Hardware control. CC input mapping from knobs/faders to
composition.data; patterns read and write the same dict viap.datafor both external data access and cross-pattern communication. OSC for bidirectional communication with mixers, lighting, visuals. - Live coding. Hot-swap patterns, change tempo, mute/unmute, and tweak parameters during playback via a built-in TCP eval server.
- Hotkeys. Single keystrokes to jump sections, toggle mutes, or fire any action - with optional bar-boundary quantization.
- Real-time pattern triggering.
composition.trigger()generates one-shot patterns in response to sensors, OSC, or any event. - Terminal display. Live status line (BPM, bar, section, chord).
Add
grid=Truefor an ASCII pattern grid showing velocity and sustain - makes legato and staccato visually distinct at a glance. Addgrid_scale=2to zoom in horizontally, revealing swing and groove micro-timing. - Web UI Dashboard (Beta). Enable with
composition.web_ui()to broadcast live composition metadata and visualize piano-roll pattern grids in a reactive HTTP/WebSocket browser dashboard. - Ableton Link. Industry-standard wireless tempo/phase sync
(
comp.link(); requirespip install subsequence[link]). Any Link-enabled app on the same LAN — Ableton Live, iOS synths, other Subsequence instances — stays in time automatically. - Recording. Record to standard MIDI file. Render to file without waiting for real-time playback.
Minimal example:
import subsequence import subsequence.constants.instruments.gm_drums as gm_drums comp = subsequence.Composition(bpm=120) @comp.pattern(channel=10, beats=4, drum_note_map=gm_drums.GM_DRUM_MAP) def drums (p): (p.hit_steps("kick_1", [0, 4, 8, 12], velocity=100) .hit_steps("snare_1", [4, 12], velocity=90) .hit_steps("hi_hat_closed", range(16), velocity=70)) comp.play()
Community and Feedback:
- Discussions: Chat and ask questions at https://github.com/simonholliday/subsequence/discussions
- Issues: Report bugs and request features at https://github.com/simonholliday/subsequence/issues
Package-level exports: Composition, Groove, MelodicState, Tuning, register_scale, scale_notes, bank_select.
1""" 2Subsequence - an algorithmic composition framework for Python. 3 4Subsequence gives you a palette of mathematical building blocks - 5Euclidean rhythms, cellular automata, L-systems, Markov chains, 6cognitive melody generation - and a stateful engine that lets them 7interact and evolve over time. Unlike tools that loop a fixed pattern 8forever, Subsequence rebuilds every pattern fresh before each cycle 9with full context, so algorithms feed into each other and compositions 10emerge that no single technique could produce alone. It generates pure 11MIDI (no audio engine) to control hardware synths, modular systems, 12drum machines, or software VSTs/DAWs. 13 14What makes it different: 15 16- **A rich algorithmic palette.** Euclidean and Bresenham rhythm 17 generators, cellular automata (1D and 2D), L-system string rewriting, 18 Markov chains, cognitive melody via the Narmour model, probability- 19 weighted ghost notes, position-aware thinning, drones and continuous 20 notes, Perlin and pink noise, logistic chaos maps - plus groove 21 templates, velocity shaping, and pitch-bend automation to shape 22 how they sound. 23- **Stateful patterns that evolve.** Each pattern is a Python function 24 rebuilt fresh every cycle with full context - current chord, section, 25 cycle count, shared data from other patterns. A Euclidean rhythm can 26 thin itself as tension builds, a cellular automaton can seed from the 27 harmony, and a Markov chain can shift behaviour between sections. 28- **Optional chord graph.** Define weighted chord and key transitions 29 via probability graphs, with gravity and automatic voice leading. 30 Eleven built-in palettes and frozen progressions to lock some sections 31 while others evolve freely. Layer on cognitive harmony for 32 Narmour-based melodic inertia. 33- **Sub-microsecond clock.** Hybrid sleep+spin timing achieves typical 34 pulse jitter of < 5 us on Linux, with zero long-term drift. 35- **Turn anything into music.** ``composition.schedule()`` runs any 36 Python function on a beat cycle - APIs, sensors, files. Anything 37 Python can reach becomes a musical parameter. 38- **Pure MIDI, zero sound engine.** No audio synthesis, no heavyweight 39 dependencies. Route to hardware synths, drum machines, Eurorack, or 40 software instruments. 41 42Composition tools: 43 44- **Rhythm and feel.** Euclidean and Bresenham generators, multi-voice 45 weighted Bresenham distribution (``bresenham_poly()``), ghost note 46 layers (``ghost_fill()``), position-aware note removal (``thin()`` - 47 the musical inverse of ``ghost_fill``), evolving cellular-automaton 48 rhythms (``cellular_1d()``, ``cellular_2d()``), smooth Perlin noise (``perlin_1d()``, 49 ``perlin_2d()``, ``perlin_1d_sequence()``, ``perlin_2d_grid()``), 50 deterministic chaos sequences (``logistic_map()``), pink 1/f noise 51 (``pink_noise()``), L-system string rewriting (``p.lsystem()``), 52 Markov-chain generation (``p.markov()``), aperiodic binary rhythms 53 (``p.thue_morse()``), golden-ratio beat placement (``p.fibonacci()``), 54 Gray-Scott reaction-diffusion patterns (``p.reaction_diffusion()``), 55 Lorenz strange-attractor generation (``p.lorenz()``), exhaustive 56 pitch-subsequence melodies (``p.de_bruijn()``), step-wise melodies 57 with guaranteed pitch diversity (``p.self_avoiding_walk()``), drones 58 and explicit note on/off events (``p.drone()``, ``p.drone_off()``, 59 ``p.silence()``), 60 groove templates (``Groove.swing()``, ``Groove.from_agr()``), swing via 61 ``p.swing()`` (a shortcut for ``Groove.swing()``), randomize, 62 velocity shaping and ramps (``p.velocity_ramp()``), dropout, per-step 63 probability, and polyrhythms via independent pattern lengths. 64- **Melody generation.** ``p.melody()`` with ``MelodicState`` applies 65 the Narmour Implication-Realization model to single-note lines: 66 continuation after small steps, reversal after large leaps, chord-tone 67 weighting, range gravity, and pitch-diversity penalty. History persists 68 across bar rebuilds for natural phrase continuity. 69- **Expression.** CC messages/ramps, pitch bend, note-correlated 70 bend/portamento/slide, program changes, SysEx, and OSC output - all 71 from within patterns. 72- **Form and structure.** Musical form as a weighted graph, ordered list, 73 or generator. Patterns read ``p.section`` to adapt. Conductor signals 74 (LFOs, ramps) shape intensity over time. 75- **Mini-notation.** ``p.seq("x x [x x] x", pitch="kick")`` - concise 76 string syntax for rhythms, subdivisions, and per-step probability. 77- **Scales and quantization.** ``p.quantize()`` snaps notes to any 78 scale. ``scale_notes()`` generates a list of MIDI note numbers from 79 a key, mode, and range or note count - useful for arpeggios, Markov 80 chains, and melodic walks. Built-in western and non-western modes, 81 plus ``register_scale()`` for your own. 82- **Microtonal tuning.** ``composition.tuning()`` applies a tuning 83 system globally; ``p.apply_tuning()`` overrides per-pattern. 84 Supports Scala ``.scl`` files, explicit cent lists, frequency ratios, 85 and N-TET equal temperaments. Polyphonic parts use explicit channel 86 rotation so simultaneous notes can carry independent pitch bends 87 without MPE. Compatible with any standard MIDI synthesiser. 88- **Randomness tools.** Weighted choice, no-repeat shuffle, random 89 walk, probability gates. Deterministic seeding (``seed=42``) makes 90 every decision repeatable. 91- **Pattern transforms.** Legato, staccato, reverse, double/half-time, 92 shift, transpose, invert, randomize, and conditional ``p.every()``. 93 94Integration: 95 96- **MIDI clock.** Master (``clock_output()``) or follower 97 (``clock_follow=True``). When multiple inputs are connected, only 98 one may be designated as the master clock source; messages from 99 other inputs are filtered to prevent sync interference. Sync to a 100 DAW or drive hardware. 101- **Hardware control.** CC input mapping from knobs/faders to 102 ``composition.data``; patterns read and write the same dict via 103 ``p.data`` for both external data access and cross-pattern 104 communication. OSC for bidirectional communication with mixers, 105 lighting, visuals. 106- **Live coding.** Hot-swap patterns, change tempo, mute/unmute, and 107 tweak parameters during playback via a built-in TCP eval server. 108- **Hotkeys.** Single keystrokes to jump sections, toggle mutes, or 109 fire any action - with optional bar-boundary quantization. 110- **Real-time pattern triggering.** ``composition.trigger()`` generates 111 one-shot patterns in response to sensors, OSC, or any event. 112- **Terminal display.** Live status line (BPM, bar, section, chord). 113 Add ``grid=True`` for an ASCII pattern grid showing velocity and 114 sustain - makes legato and staccato visually distinct at a glance. 115 Add ``grid_scale=2`` to zoom in horizontally, revealing swing and 116 groove micro-timing. 117- **Web UI Dashboard (Beta).** Enable with ``composition.web_ui()`` to 118 broadcast live composition metadata and visualize piano-roll pattern 119 grids in a reactive HTTP/WebSocket browser dashboard. 120- **Ableton Link.** Industry-standard wireless tempo/phase sync 121 (``comp.link()``; requires ``pip install subsequence[link]``). 122 Any Link-enabled app on the same LAN — Ableton Live, iOS synths, 123 other Subsequence instances — stays in time automatically. 124- **Recording.** Record to standard MIDI file. Render to file without 125 waiting for real-time playback. 126 127Minimal example: 128 129 ```python 130 import subsequence 131 import subsequence.constants.instruments.gm_drums as gm_drums 132 133 comp = subsequence.Composition(bpm=120) 134 135 @comp.pattern(channel=10, beats=4, drum_note_map=gm_drums.GM_DRUM_MAP) 136 def drums (p): 137 (p.hit_steps("kick_1", [0, 4, 8, 12], velocity=100) 138 .hit_steps("snare_1", [4, 12], velocity=90) 139 .hit_steps("hi_hat_closed", range(16), velocity=70)) 140 141 comp.play() 142 ``` 143 144Community and Feedback: 145 146- **Discussions:** Chat and ask questions at https://github.com/simonholliday/subsequence/discussions 147- **Issues:** Report bugs and request features at https://github.com/simonholliday/subsequence/issues 148 149Package-level exports: ``Composition``, ``Groove``, ``MelodicState``, ``Tuning``, ``register_scale``, ``scale_notes``, ``bank_select``. 150""" 151 152import subsequence.composition 153import subsequence.groove 154import subsequence.intervals 155import subsequence.melodic_state 156import subsequence.midi_utils 157import subsequence.tuning 158 159 160Composition = subsequence.composition.Composition 161Groove = subsequence.groove.Groove 162MelodicState = subsequence.melodic_state.MelodicState 163Tuning = subsequence.tuning.Tuning 164register_scale = subsequence.intervals.register_scale 165scale_notes = subsequence.intervals.scale_notes 166bank_select = subsequence.midi_utils.bank_select
565class Composition: 566 567 """ 568 The top-level controller for a musical piece. 569 570 The `Composition` object manages the global clock (Sequencer), the harmonic 571 progression (HarmonicState), the song structure (subsequence.form_state.FormState), and all MIDI patterns. 572 It serves as the main entry point for defining your music. 573 574 Typical workflow: 575 1. Initialize `Composition` with BPM and Key. 576 2. Define harmony and form (optional). 577 3. Register patterns using the `@composition.pattern` decorator. 578 4. Call `composition.play()` to start the music. 579 """ 580 581 def __init__ ( 582 self, 583 output_device: typing.Optional[str] = None, 584 bpm: float = 120, 585 time_signature: typing.Tuple[int, int] = (4, 4), 586 key: typing.Optional[str] = None, 587 seed: typing.Optional[int] = None, 588 record: bool = False, 589 record_filename: typing.Optional[str] = None, 590 zero_indexed_channels: bool = False 591 ) -> None: 592 593 """ 594 Initialize a new composition. 595 596 Parameters: 597 output_device: The name of the MIDI output port to use. If `None`, 598 Subsequence will attempt to find a device, prompting if necessary. 599 bpm: Initial tempo in beats per minute (default 120). 600 key: The root key of the piece (e.g., "C", "F#", "Bb"). 601 Required if you plan to use `harmony()`. 602 seed: An optional integer for deterministic randomness. When set, 603 every random decision (chord choices, drum probability, etc.) 604 will be identical on every run. 605 record: When True, record all MIDI events to a file. 606 record_filename: Optional filename for the recording (defaults to timestamp). 607 zero_indexed_channels: When False (default), MIDI channels use 608 1-based numbering (1-16) matching instrument labelling. 609 Channel 10 is drums, the way musicians and hardware panels 610 show it. When True, channels use 0-based numbering (0-15) 611 matching the raw MIDI protocol. 612 613 Example: 614 ```python 615 comp = subsequence.Composition(bpm=128, key="Eb", seed=123) 616 ``` 617 """ 618 619 self.output_device = output_device 620 self.bpm = bpm 621 self.time_signature = time_signature 622 self.key = key 623 self._seed: typing.Optional[int] = seed 624 self._zero_indexed_channels: bool = zero_indexed_channels 625 626 self._sequencer = subsequence.sequencer.Sequencer( 627 output_device_name = output_device, 628 initial_bpm = bpm, 629 time_signature = time_signature, 630 record = record, 631 record_filename = record_filename 632 ) 633 634 self._harmonic_state: typing.Optional[subsequence.harmonic_state.HarmonicState] = None 635 self._harmony_cycle_beats: typing.Optional[int] = None 636 self._harmony_reschedule_lookahead: float = 1 637 self._section_progressions: typing.Dict[str, Progression] = {} 638 self._pending_patterns: typing.List[_PendingPattern] = [] 639 self._pending_scheduled: typing.List[_PendingScheduled] = [] 640 self._form_state: typing.Optional[subsequence.form_state.FormState] = None 641 self._builder_bar: int = 0 642 self._display: typing.Optional[subsequence.display.Display] = None 643 self._live_server: typing.Optional[subsequence.live_server.LiveServer] = None 644 self._is_live: bool = False 645 self._running_patterns: typing.Dict[str, typing.Any] = {} 646 self._input_device: typing.Optional[str] = None 647 self._input_device_alias: typing.Optional[str] = None 648 self._clock_follow: bool = False 649 self._clock_output: bool = False 650 self._cc_mappings: typing.List[typing.Dict[str, typing.Any]] = [] 651 self._cc_forwards: typing.List[typing.Dict[str, typing.Any]] = [] 652 # Additional output devices registered with midi_output() after construction. 653 # Each entry: (device_name: str, alias: Optional[str]) 654 self._additional_outputs: typing.List[typing.Tuple[str, typing.Optional[str]]] = [] 655 # Additional input devices: (device_name: str, alias: Optional[str], clock_follow: bool) 656 self._additional_inputs: typing.List[typing.Tuple[str, typing.Optional[str], bool]] = [] 657 # Maps alias/name → output device index (populated in _run after all devices are opened). 658 self._output_device_names: typing.Dict[str, int] = {} 659 # Maps alias/name → input device index (populated in _run after all input devices are opened). 660 self._input_device_names: typing.Dict[str, int] = {} 661 self.data: typing.Dict[str, typing.Any] = {} 662 self._osc_server: typing.Optional[subsequence.osc.OscServer] = None 663 self.conductor = subsequence.conductor.Conductor() 664 self._web_ui_enabled: bool = False 665 self._web_ui_server: typing.Optional[subsequence.web_ui.WebUI] = None 666 self._link_quantum: typing.Optional[float] = None 667 668 # Hotkey state — populated by hotkeys() and hotkey(). 669 self._hotkeys_enabled: bool = False 670 self._hotkey_bindings: typing.Dict[str, HotkeyBinding] = {} 671 self._pending_hotkey_actions: typing.List[_PendingHotkeyAction] = [] 672 self._keystroke_listener: typing.Optional[subsequence.keystroke.KeystrokeListener] = None 673 674 # Tuning state — populated by tuning(). 675 self._tuning: typing.Optional[typing.Any] = None # subsequence.tuning.Tuning 676 self._tuning_bend_range: float = 2.0 677 self._tuning_channels: typing.Optional[typing.List[int]] = None 678 self._tuning_reference_note: int = 60 679 self._tuning_exclude_drums: bool = True 680 681 def _resolve_device_id (self, device: subsequence.midi_utils.DeviceId) -> int: 682 """Resolve an output device id (None/int/str) to an integer index. 683 684 ``None`` → 0 (primary device). ``int`` → returned as-is. 685 ``str`` → looked up in ``_output_device_names``; logs a warning and 686 returns 0 if the name is unknown (called after all devices are opened 687 in ``_run()``). 688 """ 689 if device is None: 690 return 0 691 if isinstance(device, int): 692 return device 693 idx = self._output_device_names.get(device) 694 if idx is None: 695 logger.warning( 696 f"Unknown output device name '{device}' — routing to device 0. " 697 f"Available names: {list(self._output_device_names.keys())}" 698 ) 699 return 0 700 return idx 701 702 def _resolve_input_device_id (self, device: subsequence.midi_utils.DeviceId) -> typing.Optional[int]: 703 """Resolve an input device id (None/int/str) to an integer index. 704 705 ``None`` → ``None`` (matches any input device — existing behaviour). 706 ``int`` → returned as-is. ``str`` → looked up in ``_input_device_names``; 707 logs a warning and returns ``None`` if the name is unknown. 708 Called after all input devices are opened in ``_run()``. 709 """ 710 if device is None: 711 return None 712 if isinstance(device, int): 713 return device 714 idx = self._input_device_names.get(device) 715 if idx is None: 716 logger.warning( 717 f"Unknown input device name '{device}' — mapping will be ignored. " 718 f"Available names: {list(self._input_device_names.keys())}" 719 ) 720 return None 721 return idx 722 723 def _resolve_pending_devices (self) -> None: 724 """Resolve name-based device ids on pending patterns now that all output devices are open.""" 725 for pending in self._pending_patterns: 726 if isinstance(pending.raw_device, str): 727 pending.device = self._resolve_device_id(pending.raw_device) 728 729 def _resolve_channel (self, channel: int) -> int: 730 731 """ 732 Convert a user-supplied MIDI channel to the 0-indexed value used internally. 733 734 When ``zero_indexed_channels`` is False (default), the channel is 735 validated as 1-16 and decremented by one. When True (0-indexed), the 736 channel is validated as 0-15 and returned unchanged. 737 """ 738 739 if self._zero_indexed_channels: 740 if not 0 <= channel <= 15: 741 raise ValueError(f"MIDI channel must be 0-15 (zero_indexed_channels=True), got {channel}") 742 return channel 743 else: 744 if not 1 <= channel <= 16: 745 raise ValueError(f"MIDI channel must be 1-16, got {channel}") 746 return channel - 1 747 748 @property 749 def harmonic_state (self) -> typing.Optional[subsequence.harmonic_state.HarmonicState]: 750 """The active ``HarmonicState``, or ``None`` if ``harmony()`` has not been called.""" 751 return self._harmonic_state 752 753 @property 754 def form_state (self) -> typing.Optional["subsequence.form_state.FormState"]: 755 """The active ``subsequence.form_state.FormState``, or ``None`` if ``form()`` has not been called.""" 756 return self._form_state 757 758 @property 759 def sequencer (self) -> subsequence.sequencer.Sequencer: 760 """The underlying ``Sequencer`` instance.""" 761 return self._sequencer 762 763 @property 764 def running_patterns (self) -> typing.Dict[str, typing.Any]: 765 """The currently active patterns, keyed by name.""" 766 return self._running_patterns 767 768 @property 769 def builder_bar (self) -> int: 770 """Current bar index used by pattern builders.""" 771 return self._builder_bar 772 773 def _require_harmonic_state (self) -> subsequence.harmonic_state.HarmonicState: 774 """Return the active HarmonicState, raising ValueError if none is configured.""" 775 if self._harmonic_state is None: 776 raise ValueError( 777 "harmony() must be called before this action — " 778 "no harmonic state has been configured." 779 ) 780 return self._harmonic_state 781 782 def harmony ( 783 self, 784 style: typing.Union[str, subsequence.chord_graphs.ChordGraph] = "functional_major", 785 cycle_beats: int = 4, 786 dominant_7th: bool = True, 787 gravity: float = 1.0, 788 nir_strength: float = 0.5, 789 minor_turnaround_weight: float = 0.0, 790 root_diversity: float = subsequence.harmonic_state.DEFAULT_ROOT_DIVERSITY, 791 reschedule_lookahead: float = 1 792 ) -> None: 793 794 """ 795 Configure the harmonic logic and chord change intervals. 796 797 Subsequence uses a weighted transition graph to choose the next chord. 798 You can influence these choices using 'gravity' (favoring the tonic) and 799 'NIR strength' (melodic inertia based on Narmour's model). 800 801 Parameters: 802 style: The harmonic style to use. Built-in: "functional_major" 803 (alias "diatonic_major"), "turnaround", "aeolian_minor", 804 "phrygian_minor", "lydian_major", "dorian_minor", 805 "chromatic_mediant", "suspended", "mixolydian", "whole_tone", 806 "diminished". See README for full descriptions. 807 cycle_beats: How many beats each chord lasts (default 4). 808 dominant_7th: Whether to include V7 chords (default True). 809 gravity: Key gravity (0.0 to 1.0). High values stay closer to the root chord. 810 nir_strength: Melodic inertia (0.0 to 1.0). Influences chord movement 811 expectations. 812 minor_turnaround_weight: For "turnaround" style, influences major vs minor feel. 813 root_diversity: Root-repetition damping (0.0 to 1.0). Each recent 814 chord sharing a candidate's root reduces the weight to 40% at 815 the default (0.4). Set to 1.0 to disable. 816 reschedule_lookahead: How many beats in advance to calculate the 817 next chord. 818 819 Example: 820 ```python 821 # A moody minor progression that changes every 8 beats 822 comp.harmony(style="aeolian_minor", cycle_beats=8, gravity=0.4) 823 ``` 824 """ 825 826 if self.key is None: 827 raise ValueError("Cannot configure harmony without a key - set key in the Composition constructor") 828 829 preserved_history: typing.List[subsequence.chords.Chord] = [] 830 preserved_current: typing.Optional[subsequence.chords.Chord] = None 831 832 if self._harmonic_state is not None: 833 preserved_history = self._harmonic_state.history.copy() 834 preserved_current = self._harmonic_state.current_chord 835 836 self._harmonic_state = subsequence.harmonic_state.HarmonicState( 837 key_name = self.key, 838 graph_style = style, 839 include_dominant_7th = dominant_7th, 840 key_gravity_blend = gravity, 841 nir_strength = nir_strength, 842 minor_turnaround_weight = minor_turnaround_weight, 843 root_diversity = root_diversity 844 ) 845 846 if preserved_history: 847 self._harmonic_state.history = preserved_history 848 if preserved_current is not None and self._harmonic_state.graph.get_transitions(preserved_current): 849 self._harmonic_state.current_chord = preserved_current 850 851 self._harmony_cycle_beats = cycle_beats 852 self._harmony_reschedule_lookahead = reschedule_lookahead 853 854 def freeze (self, bars: int) -> "Progression": 855 856 """Capture a chord progression from the live harmony engine. 857 858 Runs the harmony engine forward by *bars* chord changes, records each 859 chord, and returns it as a :class:`Progression` that can be bound to a 860 form section with :meth:`section_chords`. 861 862 The engine state **advances** — successive ``freeze()`` calls produce a 863 continuing compositional journey so section progressions feel like parts 864 of a whole rather than isolated islands. 865 866 Parameters: 867 bars: Number of chords to capture (one per harmony cycle). 868 869 Returns: 870 A :class:`Progression` with the captured chords and trailing 871 history for NIR continuity. 872 873 Raises: 874 ValueError: If :meth:`harmony` has not been called first. 875 876 Example:: 877 878 composition.harmony(style="functional_major", cycle_beats=4) 879 verse = composition.freeze(8) # 8 chords, engine advances 880 chorus = composition.freeze(4) # next 4 chords, continuing on 881 composition.section_chords("verse", verse) 882 composition.section_chords("chorus", chorus) 883 """ 884 885 hs = self._require_harmonic_state() 886 887 if bars < 1: 888 raise ValueError("bars must be at least 1") 889 collected: typing.List[subsequence.chords.Chord] = [hs.current_chord] 890 891 for _ in range(bars - 1): 892 hs.step() 893 collected.append(hs.current_chord) 894 895 # Advance past the last captured chord so the next freeze() call or 896 # live playback does not duplicate it. 897 hs.step() 898 899 return Progression( 900 chords = tuple(collected), 901 trailing_history = tuple(hs.history), 902 ) 903 904 def section_chords (self, section_name: str, progression: "Progression") -> None: 905 906 """Bind a frozen :class:`Progression` to a named form section. 907 908 Every time *section_name* plays, the harmonic clock replays the 909 progression's chords in order instead of calling the live engine. 910 Sections without a bound progression continue generating live chords. 911 912 Parameters: 913 section_name: Name of the section as defined in :meth:`form`. 914 progression: The :class:`Progression` returned by :meth:`freeze`. 915 916 Raises: 917 ValueError: If the form has been configured and *section_name* is 918 not a known section name. 919 920 Example:: 921 922 composition.section_chords("verse", verse_progression) 923 composition.section_chords("chorus", chorus_progression) 924 # "bridge" is not bound — it generates live chords 925 """ 926 927 if ( 928 self._form_state is not None 929 and self._form_state._section_bars is not None 930 and section_name not in self._form_state._section_bars 931 ): 932 known = ", ".join(sorted(self._form_state._section_bars)) 933 raise ValueError( 934 f"Section '{section_name}' not found in form. " 935 f"Known sections: {known}" 936 ) 937 938 self._section_progressions[section_name] = progression 939 940 def on_event (self, event_name: str, callback: typing.Callable[..., typing.Any]) -> None: 941 942 """ 943 Register a callback for a sequencer event (e.g., "bar", "start", "stop"). 944 """ 945 946 self._sequencer.on_event(event_name, callback) 947 948 949 # ----------------------------------------------------------------------- 950 # Hotkey API 951 # ----------------------------------------------------------------------- 952 953 def hotkeys (self, enabled: bool = True) -> None: 954 955 """Enable or disable the global hotkey listener. 956 957 Must be called **before** :meth:`play` to take effect. When enabled, a 958 background thread reads single keystrokes from stdin without requiring 959 Enter. The ``?`` key is always reserved and lists all active bindings. 960 961 Hotkeys have zero impact on playback when disabled — the listener 962 thread is never started. 963 964 Args: 965 enabled: ``True`` (default) to enable hotkeys; ``False`` to disable. 966 967 Example:: 968 969 composition.hotkeys() 970 composition.hotkey("a", lambda: composition.form_jump("chorus")) 971 composition.play() 972 """ 973 974 self._hotkeys_enabled = enabled 975 976 977 def hotkey ( 978 self, 979 key: str, 980 action: typing.Callable[[], None], 981 quantize: int = 0, 982 label: typing.Optional[str] = None, 983 ) -> None: 984 985 """Register a single-key shortcut that fires during playback. 986 987 The listener must be enabled first with :meth:`hotkeys`. 988 989 Most actions — form jumps, ``composition.data`` writes, and 990 :meth:`tweak` calls — should use ``quantize=0`` (the default). Their 991 musical effect is naturally delayed to the next pattern rebuild cycle, 992 which provides automatic musical quantization without extra configuration. 993 994 Use ``quantize=N`` for actions where you want an explicit bar-boundary 995 guarantee, such as :meth:`mute` / :meth:`unmute`. 996 997 The ``?`` key is reserved and cannot be overridden. 998 999 Args: 1000 key: A single character trigger (e.g. ``"a"``, ``"1"``, ``" "``). 1001 action: Zero-argument callable to execute. 1002 quantize: ``0`` = execute immediately (default). ``N`` = execute 1003 on the next global bar number divisible by *N*. 1004 label: Display name for the ``?`` help listing. Auto-derived from 1005 the function name or lambda body if omitted. 1006 1007 Raises: 1008 ValueError: If ``key`` is the reserved ``?`` character, or if 1009 ``key`` is not exactly one character. 1010 1011 Example:: 1012 1013 composition.hotkeys() 1014 1015 # Immediate — musical effect happens at next pattern rebuild 1016 composition.hotkey("a", lambda: composition.form_jump("chorus")) 1017 composition.hotkey("1", lambda: composition.data.update({"mode": "chill"})) 1018 1019 # Explicit 4-bar phrase boundary 1020 composition.hotkey("s", lambda: composition.mute("drums"), quantize=4) 1021 1022 # Named function — label is derived automatically 1023 def drop_to_breakdown (): 1024 composition.form_jump("breakdown") 1025 composition.mute("lead") 1026 1027 composition.hotkey("d", drop_to_breakdown) 1028 1029 composition.play() 1030 """ 1031 1032 if len(key) != 1: 1033 raise ValueError(f"hotkey key must be a single character, got {key!r}") 1034 1035 if key == _HOTKEY_RESERVED: 1036 raise ValueError(f"'{_HOTKEY_RESERVED}' is reserved for listing active hotkeys.") 1037 1038 derived = label if label is not None else _derive_label(action) 1039 1040 self._hotkey_bindings[key] = HotkeyBinding( 1041 key = key, 1042 action = action, 1043 quantize = quantize, 1044 label = derived, 1045 ) 1046 1047 1048 def form_jump (self, section_name: str) -> None: 1049 1050 """Jump the form to a named section immediately. 1051 1052 Delegates to :meth:`subsequence.form_state.FormState.jump_to`. Only works when the 1053 composition uses graph-mode form (a dict passed to :meth:`form`). 1054 1055 The musical effect is heard at the *next pattern rebuild cycle* — already- 1056 queued MIDI notes are unaffected. This natural delay means ``form_jump`` 1057 is effective without needing explicit quantization. 1058 1059 Args: 1060 section_name: The section to jump to. 1061 1062 Raises: 1063 ValueError: If no form is configured, or the form is not in graph 1064 mode, or *section_name* is unknown. 1065 1066 Example:: 1067 1068 composition.hotkey("c", lambda: composition.form_jump("chorus")) 1069 """ 1070 1071 if self._form_state is None: 1072 raise ValueError("form_jump() requires a form to be configured via composition.form().") 1073 1074 self._form_state.jump_to(section_name) 1075 1076 1077 def form_next (self, section_name: str) -> None: 1078 1079 """Queue the next section — takes effect when the current section ends. 1080 1081 Unlike :meth:`form_jump`, this does not interrupt the current section. 1082 The queued section replaces the automatically pre-decided next section 1083 and takes effect at the natural section boundary. The performer can 1084 change their mind by calling ``form_next`` again before the boundary. 1085 1086 Delegates to :meth:`subsequence.form_state.FormState.queue_next`. Only works when the 1087 composition uses graph-mode form (a dict passed to :meth:`form`). 1088 1089 Args: 1090 section_name: The section to queue. 1091 1092 Raises: 1093 ValueError: If no form is configured, or the form is not in graph 1094 mode, or *section_name* is unknown. 1095 1096 Example:: 1097 1098 composition.hotkey("c", lambda: composition.form_next("chorus")) 1099 """ 1100 1101 if self._form_state is None: 1102 raise ValueError("form_next() requires a form to be configured via composition.form().") 1103 1104 self._form_state.queue_next(section_name) 1105 1106 1107 def _list_hotkeys (self) -> None: 1108 1109 """Log all active hotkey bindings (triggered by the ``?`` key). 1110 1111 Output appears via the standard logger so it scrolls cleanly above 1112 the :class:`~subsequence.display.Display` status line. 1113 """ 1114 1115 lines = ["Active hotkeys:"] 1116 for key in sorted(self._hotkey_bindings): 1117 b = self._hotkey_bindings[key] 1118 quant_str = "immediate" if b.quantize == 0 else f"quantize={b.quantize}" 1119 lines.append(f" {key} \u2192 {b.label} ({quant_str})") 1120 lines.append(f" ? \u2192 list hotkeys") 1121 logger.info("\n".join(lines)) 1122 1123 1124 def _process_hotkeys (self, bar: int) -> None: 1125 1126 """Drain pending keystrokes and execute due actions. 1127 1128 Called on every ``"bar"`` event by the sequencer when hotkeys are 1129 enabled. Handles both immediate (``quantize=0``) and quantized actions. 1130 1131 Immediate actions are executed directly from the keystroke listener 1132 thread (not here). This method only processes quantized actions that 1133 were deferred to a bar boundary. 1134 1135 Args: 1136 bar: The current global bar number from the sequencer. 1137 """ 1138 1139 if self._keystroke_listener is None: 1140 return 1141 1142 # Process newly arrived keys. 1143 for key in self._keystroke_listener.drain(): 1144 1145 if key == _HOTKEY_RESERVED: 1146 self._list_hotkeys() 1147 continue 1148 1149 binding = self._hotkey_bindings.get(key) 1150 if binding is None: 1151 continue 1152 1153 if binding.quantize == 0: 1154 # Immediate — execute now (we're on the bar-event callback, 1155 # which is safe for all mutation methods). 1156 try: 1157 binding.action() 1158 logger.info(f"Hotkey '{key}' \u2192 {binding.label}") 1159 except Exception as exc: 1160 logger.warning(f"Hotkey '{key}' action raised: {exc}") 1161 else: 1162 # Defer until the next quantize boundary. 1163 self._pending_hotkey_actions.append( 1164 _PendingHotkeyAction(binding=binding, requested_bar=bar) 1165 ) 1166 1167 # Fire any pending actions whose bar boundary has arrived. 1168 still_pending: typing.List[_PendingHotkeyAction] = [] 1169 1170 for pending in self._pending_hotkey_actions: 1171 if bar % pending.binding.quantize == 0: 1172 try: 1173 pending.binding.action() 1174 logger.info( 1175 f"Hotkey '{pending.binding.key}' \u2192 {pending.binding.label} " 1176 f"(bar {bar})" 1177 ) 1178 except Exception as exc: 1179 logger.warning( 1180 f"Hotkey '{pending.binding.key}' action raised: {exc}" 1181 ) 1182 else: 1183 still_pending.append(pending) 1184 1185 self._pending_hotkey_actions = still_pending 1186 1187 def seed (self, value: int) -> None: 1188 1189 """ 1190 Set a random seed for deterministic, repeatable playback. 1191 1192 If a seed is set, Subsequence will produce the exact same sequence 1193 every time you run the script. This is vital for finishing tracks or 1194 reproducing a specific 'performance'. 1195 1196 Parameters: 1197 value: An integer seed. 1198 1199 Example: 1200 ```python 1201 # Fix the randomness 1202 comp.seed(42) 1203 ``` 1204 """ 1205 1206 self._seed = value 1207 1208 def tuning ( 1209 self, 1210 source: typing.Optional[typing.Union[str, "os.PathLike"]] = None, 1211 *, 1212 cents: typing.Optional[typing.List[float]] = None, 1213 ratios: typing.Optional[typing.List[float]] = None, 1214 equal: typing.Optional[int] = None, 1215 bend_range: float = 2.0, 1216 channels: typing.Optional[typing.List[int]] = None, 1217 reference_note: int = 60, 1218 exclude_drums: bool = True, 1219 ) -> None: 1220 1221 """Set a global microtonal tuning for the composition. 1222 1223 The tuning is applied automatically after each pattern rebuild (before 1224 the pattern is scheduled). Drum patterns (those registered with a 1225 ``drum_note_map``) are excluded by default. 1226 1227 Supply exactly one of the source parameters: 1228 1229 - ``source``: path to a Scala ``.scl`` file. 1230 - ``cents``: list of cent offsets for degrees 1..N (degree 0 = 0.0 is implicit). 1231 - ``ratios``: list of frequency ratios (e.g., ``[9/8, 5/4, 4/3, 3/2, 2]``). 1232 - ``equal``: integer for N-tone equal temperament (e.g., ``equal=19``). 1233 1234 For polyphonic parts, supply a ``channels`` pool. Notes are spread 1235 across those MIDI channels so each can carry an independent pitch bend. 1236 The synth must be configured to match ``bend_range`` (its pitch-bend range 1237 setting in semitones). 1238 1239 Parameters: 1240 source: Path to a ``.scl`` file. 1241 cents: Cent offsets for scale degrees 1..N. 1242 ratios: Frequency ratios for scale degrees 1..N. 1243 equal: Number of equal divisions of the period. 1244 bend_range: Synth pitch-bend range in semitones (default ±2). 1245 channels: Channel pool for polyphonic rotation. 1246 reference_note: MIDI note mapped to scale degree 0 (default 60 = C4). 1247 exclude_drums: When True (default), skip patterns that have a 1248 ``drum_note_map`` (they use fixed GM pitches, not tuned ones). 1249 1250 Example: 1251 ```python 1252 # Quarter-comma meantone from a Scala file 1253 comp.tuning("meanquar.scl") 1254 1255 # Just intonation from ratios 1256 comp.tuning(ratios=[9/8, 5/4, 4/3, 3/2, 5/3, 15/8, 2]) 1257 1258 # 19-TET, monophonic 1259 comp.tuning(equal=19, bend_range=2.0) 1260 1261 # 31-TET with channel rotation for polyphony (channels 1-6) 1262 comp.tuning("31tet.scl", channels=[0, 1, 2, 3, 4, 5]) 1263 ``` 1264 """ 1265 import subsequence.tuning as _tuning_mod 1266 1267 given = sum(x is not None for x in [source, cents, ratios, equal]) 1268 if given == 0: 1269 raise ValueError("composition.tuning() requires one of: source, cents, ratios, or equal") 1270 if given > 1: 1271 raise ValueError("composition.tuning() accepts only one source parameter") 1272 1273 if source is not None: 1274 t = _tuning_mod.Tuning.from_scl(source) 1275 elif cents is not None: 1276 t = _tuning_mod.Tuning.from_cents(cents) 1277 elif ratios is not None: 1278 t = _tuning_mod.Tuning.from_ratios(ratios) 1279 else: 1280 t = _tuning_mod.Tuning.equal(equal) # type: ignore[arg-type] 1281 1282 self._tuning = t 1283 self._tuning_bend_range = bend_range 1284 self._tuning_channels = channels 1285 self._tuning_reference_note = reference_note 1286 self._tuning_exclude_drums = exclude_drums 1287 1288 def display (self, enabled: bool = True, grid: bool = False, grid_scale: float = 1.0) -> None: 1289 1290 """ 1291 Enable or disable the live terminal dashboard. 1292 1293 When enabled, Subsequence uses a safe logging handler that allows a 1294 persistent status line (BPM, Key, Bar, Section, Chord) to stay at 1295 the bottom of the terminal while logs scroll above it. 1296 1297 Parameters: 1298 enabled: Whether to show the display (default True). 1299 grid: When True, render an ASCII grid visualisation of all 1300 running patterns above the status line. The grid updates 1301 once per bar, showing which steps have notes and at what 1302 velocity. 1303 grid_scale: Horizontal zoom factor for the grid (default 1304 ``1.0``). Higher values add visual columns between 1305 grid steps, revealing micro-timing from swing and groove. 1306 Snapped to the nearest integer internally for uniform 1307 marker spacing. 1308 """ 1309 1310 if enabled: 1311 self._display = subsequence.display.Display(self, grid=grid, grid_scale=grid_scale) 1312 else: 1313 self._display = None 1314 1315 def web_ui (self) -> None: 1316 1317 """ 1318 Enable the realtime Web UI Dashboard. 1319 1320 When enabled, Subsequence instantiates a WebSocket server that broadcasts 1321 the current state, signals, and active patterns (with high-res timing and note data) 1322 to any connected browser clients. 1323 """ 1324 1325 self._web_ui_enabled = True 1326 1327 def midi_input (self, device: str, clock_follow: bool = False, name: typing.Optional[str] = None) -> None: 1328 1329 """ 1330 Configure a MIDI input device for external sync and MIDI messages. 1331 1332 May be called multiple times to register additional input devices. 1333 The first call sets the primary input (device 0). Subsequent calls 1334 add additional input devices (device 1, 2, …). Only one device may 1335 have ``clock_follow=True``. 1336 1337 Parameters: 1338 device: The name of the MIDI input port. 1339 clock_follow: If True, Subsequence will slave its clock to incoming 1340 MIDI Ticks. It will also follow MIDI Start/Stop/Continue 1341 commands. Only one device can have this enabled at a time. 1342 name: Optional alias for use with ``cc_map(input_device=…)`` and 1343 ``cc_forward(input_device=…)``. When omitted, the raw device 1344 name is used. 1345 1346 Example: 1347 ```python 1348 # Single controller (unchanged usage) 1349 comp.midi_input("Scarlett 2i4", clock_follow=True) 1350 1351 # Multiple controllers 1352 comp.midi_input("Arturia KeyStep", name="keys") 1353 comp.midi_input("Faderfox EC4", name="faders") 1354 ``` 1355 """ 1356 1357 if clock_follow: 1358 if self.is_clock_following: 1359 raise ValueError("Only one input device can be configured to follow external clock (clock_follow=True)") 1360 1361 if self._input_device is None: 1362 # First call: set primary input device (device 0) 1363 self._input_device = device 1364 self._input_device_alias = name 1365 self._clock_follow = clock_follow 1366 else: 1367 # Subsequent calls: register additional input devices 1368 self._additional_inputs.append((device, name, clock_follow)) 1369 1370 def midi_output (self, device: str, name: typing.Optional[str] = None) -> int: 1371 1372 """ 1373 Register an additional MIDI output device. 1374 1375 The first output device is always the one passed to 1376 ``Composition(output_device=…)`` — that is device 0. 1377 Each call to ``midi_output()`` adds the next device (1, 2, …). 1378 1379 Parameters: 1380 device: The name of the MIDI output port. 1381 name: Optional alias for use with ``pattern(device=…)``, 1382 ``cc_forward(output_device=…)``, etc. When omitted, the raw 1383 device name is used. 1384 1385 Returns: 1386 The integer device index assigned (1, 2, 3, …). 1387 1388 Example: 1389 ```python 1390 comp = subsequence.Composition(bpm=120, output_device="MOTU Express") 1391 1392 # Returns 1 — use as device=1 or device="integra" 1393 comp.midi_output("Roland Integra", name="integra") 1394 1395 @comp.pattern(channel=1, beats=4, device="integra") 1396 def strings (p): 1397 p.note(60, beat=0) 1398 ``` 1399 """ 1400 1401 idx = 1 + len(self._additional_outputs) # device 0 is always the primary 1402 self._additional_outputs.append((device, name)) 1403 return idx 1404 1405 def clock_output (self, enabled: bool = True) -> None: 1406 1407 """ 1408 Send MIDI timing clock to connected hardware. 1409 1410 When enabled, Subsequence acts as a MIDI clock master and sends 1411 standard clock messages on the output port: a Start message (0xFA) 1412 when playback begins, a Clock tick (0xF8) on every pulse (24 PPQN), 1413 and a Stop message (0xFC) when playback ends. 1414 1415 This allows hardware synthesizers, drum machines, and effect units to 1416 slave their tempo to Subsequence automatically. 1417 1418 **Note:** Clock output is automatically disabled when ``midi_input()`` 1419 is called with ``clock_follow=True``, to prevent a clock feedback loop. 1420 1421 Parameters: 1422 enabled: Whether to send MIDI clock (default True). 1423 1424 Example: 1425 ```python 1426 comp = subsequence.Composition(bpm=120, output_device="...") 1427 comp.clock_output() # hardware will follow Subsequence tempo 1428 ``` 1429 """ 1430 1431 self._clock_output = enabled 1432 1433 1434 def link (self, quantum: float = 4.0) -> "Composition": 1435 1436 """ 1437 Enable Ableton Link tempo and phase synchronisation. 1438 1439 When enabled, Subsequence joins the local Link session and slaves its 1440 clock to the shared network tempo and beat phase. All other Link-enabled 1441 apps on the same LAN — Ableton Live, iOS synths, other Subsequence 1442 instances — will automatically stay in time. 1443 1444 Playback starts on the next bar boundary aligned to the Link quantum, 1445 so downbeats stay in sync across all participants. 1446 1447 Requires the ``link`` optional extra:: 1448 1449 pip install subsequence[link] 1450 1451 Parameters: 1452 quantum: Beat cycle length. ``4.0`` (default) = one bar in 4/4 time. 1453 Change this if your composition uses a different meter. 1454 1455 Example:: 1456 1457 comp = subsequence.Composition(bpm=120, key="C") 1458 comp.link() # join the Link session 1459 comp.play() 1460 1461 # On another machine / instance: 1462 comp2 = subsequence.Composition(bpm=120) 1463 comp2.link() # tempo and phase will lock to comp 1464 comp2.play() 1465 1466 Note: 1467 ``set_bpm()`` proposes the new tempo to the Link network when Link 1468 is active. The network-authoritative tempo is applied on the next 1469 pulse, so there may be a brief lag before the change is visible. 1470 """ 1471 1472 # Eagerly check that aalink is installed — fail early with a clear message. 1473 subsequence.link_clock._require_aalink() 1474 1475 self._link_quantum = quantum 1476 return self 1477 1478 1479 def cc_map ( 1480 self, 1481 cc: int, 1482 key: str, 1483 channel: typing.Optional[int] = None, 1484 min_val: float = 0.0, 1485 max_val: float = 1.0, 1486 input_device: subsequence.midi_utils.DeviceId = None, 1487 ) -> None: 1488 1489 """ 1490 Map an incoming MIDI CC to a ``composition.data`` key. 1491 1492 When the composition receives a CC message on the configured MIDI 1493 input port, the value is scaled from the CC range (0–127) to 1494 *[min_val, max_val]* and stored in ``composition.data[key]``. 1495 1496 This lets hardware knobs, faders, and expression pedals control live 1497 parameters without writing any callback code. 1498 1499 **Requires** ``midi_input()`` to be called first to open an input port. 1500 1501 Parameters: 1502 cc: MIDI Control Change number (0–127). 1503 key: The ``composition.data`` key to write. 1504 channel: If given, only respond to CC messages on this channel. 1505 Uses the same numbering convention as ``pattern()`` (0-15 1506 by default, or 1-16 with ``zero_indexed_channels=False``). 1507 ``None`` matches any channel (default). 1508 min_val: Scaled minimum — written when CC value is 0 (default 0.0). 1509 max_val: Scaled maximum — written when CC value is 127 (default 1.0). 1510 input_device: Only respond to CC messages from this input device 1511 (index or name). ``None`` responds to any input device (default). 1512 1513 Example: 1514 ```python 1515 comp.midi_input("Arturia KeyStep") 1516 comp.cc_map(74, "filter_cutoff") # knob → 0.0–1.0 1517 comp.cc_map(7, "volume", min_val=0, max_val=127) # volume fader 1518 1519 # Multi-device: only listen to CC 74 from the "faders" controller 1520 comp.cc_map(74, "filter", input_device="faders") 1521 ``` 1522 """ 1523 1524 resolved_channel = self._resolve_channel(channel) if channel is not None else None 1525 1526 self._cc_mappings.append({ 1527 'cc': cc, 1528 'key': key, 1529 'channel': resolved_channel, 1530 'min_val': min_val, 1531 'max_val': max_val, 1532 'input_device': input_device, # resolved to int index in _run() 1533 }) 1534 1535 1536 @staticmethod 1537 def _make_cc_forward_transform ( 1538 output: typing.Union[str, typing.Callable], 1539 cc: int, 1540 output_channel: typing.Optional[int], 1541 ) -> typing.Callable: 1542 1543 """Build a transform callable from a preset string or user-supplied callable. 1544 1545 The returned callable has signature ``(value: int, channel: int) -> Optional[mido.Message]`` 1546 where ``channel`` is the 0-indexed incoming channel. 1547 """ 1548 1549 import mido as _mido 1550 1551 def _out_ch (incoming: int) -> int: 1552 return output_channel if output_channel is not None else incoming 1553 1554 if callable(output): 1555 if output_channel is None: 1556 return output 1557 def _wrapped (value: int, channel: int) -> typing.Optional[typing.Any]: 1558 msg = output(value, channel) 1559 if msg is not None and output_channel is not None: 1560 # Rebuild message with overridden channel 1561 return _mido.Message(msg.type, channel=output_channel, **{ 1562 k: v for k, v in msg.__dict__.items() if k != 'channel' 1563 }) 1564 return msg 1565 return _wrapped 1566 1567 if output == 'cc': 1568 def _cc_identity (value: int, channel: int) -> typing.Any: 1569 return _mido.Message('control_change', channel=_out_ch(channel), control=cc, value=value) 1570 return _cc_identity 1571 1572 if output.startswith('cc:'): 1573 try: 1574 target_cc = int(output[3:]) 1575 except ValueError: 1576 raise ValueError(f"cc_forward(): invalid preset '{output}' — expected 'cc:N' where N is 0–127") 1577 if not 0 <= target_cc <= 127: 1578 raise ValueError(f"cc_forward(): CC number {target_cc} out of range 0–127") 1579 def _cc_remap (value: int, channel: int) -> typing.Any: 1580 return _mido.Message('control_change', channel=_out_ch(channel), control=target_cc, value=value) 1581 return _cc_remap 1582 1583 if output == 'pitchwheel': 1584 def _pitchwheel (value: int, channel: int) -> typing.Any: 1585 pitch = int(value / 127 * 16383) - 8192 1586 return _mido.Message('pitchwheel', channel=_out_ch(channel), pitch=pitch) 1587 return _pitchwheel 1588 1589 raise ValueError( 1590 f"cc_forward(): unknown preset '{output}'. " 1591 "Use 'cc', 'cc:N' (e.g. 'cc:74'), 'pitchwheel', or a callable." 1592 ) 1593 1594 1595 def cc_forward ( 1596 self, 1597 cc: int, 1598 output: typing.Union[str, typing.Callable], 1599 *, 1600 channel: typing.Optional[int] = None, 1601 output_channel: typing.Optional[int] = None, 1602 mode: str = "instant", 1603 input_device: subsequence.midi_utils.DeviceId = None, 1604 output_device: subsequence.midi_utils.DeviceId = None, 1605 ) -> None: 1606 1607 """ 1608 Forward an incoming MIDI CC to the MIDI output in real-time. 1609 1610 Unlike ``cc_map()`` which writes incoming CC values to ``composition.data`` 1611 for use at pattern rebuild time, ``cc_forward()`` routes the signal 1612 directly to the MIDI output — bypassing the pattern cycle entirely. 1613 1614 Both ``cc_map()`` and ``cc_forward()`` may be registered for the same CC 1615 number; they operate independently. 1616 1617 Parameters: 1618 cc: Incoming CC number to listen for (0–127). 1619 output: What to send. Either a **preset string**: 1620 1621 - ``"cc"`` — identity forward, same CC number and value. 1622 - ``"cc:N"`` — forward as CC number N (e.g. ``"cc:74"``). 1623 - ``"pitchwheel"`` — scale 0–127 to -8192..8191 and send as pitch bend. 1624 1625 Or a **callable** with signature 1626 ``(value: int, channel: int) -> Optional[mido.Message]``. 1627 Return a fully formed ``mido.Message`` to send, or ``None`` to suppress. 1628 ``channel`` is 0-indexed (the incoming channel). 1629 channel: If given, only respond to CC messages on this channel. 1630 Uses the same numbering convention as ``cc_map()``. 1631 ``None`` matches any channel (default). 1632 output_channel: Override the output channel. ``None`` uses the 1633 incoming channel. Uses the same numbering convention as ``pattern()``. 1634 mode: Dispatch mode: 1635 1636 - ``"instant"`` *(default)* — send immediately on the MIDI input 1637 callback thread. Lowest latency (~1–5 ms). Instant forwards are 1638 **not** recorded when recording is enabled. 1639 - ``"queued"`` — inject into the sequencer event queue and send at 1640 the next pulse boundary (~0–20 ms at 120 BPM). Queued forwards 1641 **are** recorded when recording is enabled. 1642 1643 Example: 1644 ```python 1645 comp.midi_input("Arturia KeyStep") 1646 1647 # CC 1 → CC 1 (identity, instant) 1648 comp.cc_forward(1, "cc") 1649 1650 # CC 1 → pitch bend on channel 1, queued (recordable) 1651 comp.cc_forward(1, "pitchwheel", output_channel=1, mode="queued") 1652 1653 # CC 1 → CC 74, custom channel 1654 comp.cc_forward(1, "cc:74", output_channel=2) 1655 1656 # Custom transform — remap CC range 0–127 to CC 74 range 40–100 1657 import subsequence.midi as midi 1658 comp.cc_forward(1, lambda v, ch: midi.cc(74, int(v / 127 * 60) + 40, channel=ch)) 1659 1660 # Forward AND map to data simultaneously — both active on the same CC 1661 comp.cc_map(1, "mod_wheel") 1662 comp.cc_forward(1, "cc:74") 1663 ``` 1664 """ 1665 1666 if not 0 <= cc <= 127: 1667 raise ValueError(f"cc_forward(): cc {cc} out of range 0–127") 1668 1669 if mode not in ('instant', 'queued'): 1670 raise ValueError(f"cc_forward(): mode must be 'instant' or 'queued', got '{mode}'") 1671 1672 resolved_in_channel = self._resolve_channel(channel) if channel is not None else None 1673 resolved_out_channel = self._resolve_channel(output_channel) if output_channel is not None else None 1674 1675 transform = self._make_cc_forward_transform(output, cc, resolved_out_channel) 1676 1677 self._cc_forwards.append({ 1678 'cc': cc, 1679 'channel': resolved_in_channel, 1680 'output_channel': resolved_out_channel, 1681 'mode': mode, 1682 'transform': transform, 1683 'input_device': input_device, # resolved to int index in _run() 1684 'output_device': output_device, # resolved to int index in _run() 1685 }) 1686 1687 1688 def live (self, port: int = 5555) -> None: 1689 1690 """ 1691 Enable the live coding eval server. 1692 1693 This allows you to connect to a running composition using the 1694 `subsequence.live_client` REPL and hot-swap pattern code or 1695 modify variables in real-time. 1696 1697 Parameters: 1698 port: The TCP port to listen on (default 5555). 1699 """ 1700 1701 self._live_server = subsequence.live_server.LiveServer(self, port=port) 1702 self._is_live = True 1703 1704 def osc (self, receive_port: int = 9000, send_port: int = 9001, send_host: str = "127.0.0.1") -> None: 1705 1706 """ 1707 Enable bi-directional Open Sound Control (OSC). 1708 1709 Subsequence will listen for commands (like `/bpm` or `/mute`) and 1710 broadcast its internal state (like `/chord` or `/bar`) over UDP. 1711 1712 Parameters: 1713 receive_port: Port to listen for incoming OSC messages (default 9000). 1714 send_port: Port to send state updates to (default 9001). 1715 send_host: The IP address to send updates to (default "127.0.0.1"). 1716 """ 1717 1718 self._osc_server = subsequence.osc.OscServer( 1719 self, 1720 receive_port = receive_port, 1721 send_port = send_port, 1722 send_host = send_host 1723 ) 1724 1725 def osc_map (self, address: str, handler: typing.Callable) -> None: 1726 1727 """ 1728 Register a custom OSC handler. 1729 1730 Must be called after :meth:`osc` has been configured. 1731 1732 Parameters: 1733 address: OSC address pattern to match (e.g. ``"/my/param"``). 1734 handler: Callable invoked with ``(address, *args)`` when a 1735 matching message arrives. 1736 1737 Example:: 1738 1739 composition.osc("/control") 1740 1741 def on_intensity (address, value): 1742 composition.data["intensity"] = float(value) 1743 1744 composition.osc_map("/intensity", on_intensity) 1745 """ 1746 1747 if self._osc_server is None: 1748 raise RuntimeError("Call composition.osc() before composition.osc_map()") 1749 1750 self._osc_server.map(address, handler) 1751 1752 def set_bpm (self, bpm: float) -> None: 1753 1754 """ 1755 Instantly change the tempo. 1756 1757 Parameters: 1758 bpm: The new tempo in beats per minute. 1759 1760 When Ableton Link is active, this proposes the new tempo to the Link 1761 network instead of applying it locally. The network-authoritative tempo 1762 is picked up on the next pulse. 1763 """ 1764 1765 self._sequencer.set_bpm(bpm) 1766 1767 if not self.is_clock_following and self._link_quantum is None: 1768 self.bpm = bpm 1769 1770 def target_bpm (self, bpm: float, bars: int, shape: str = "linear") -> None: 1771 1772 """ 1773 Smoothly ramp the tempo to a target value over a number of bars. 1774 1775 Parameters: 1776 bpm: Target tempo in beats per minute. 1777 bars: Duration of the transition in bars. 1778 shape: Easing curve name. Defaults to ``"linear"``. 1779 ``"ease_in_out"`` or ``"s_curve"`` are recommended for natural- 1780 sounding tempo changes. See :mod:`subsequence.easing` for all 1781 available shapes. 1782 1783 Example: 1784 ```python 1785 # Accelerate to 140 BPM over the next 8 bars with a smooth S-curve 1786 comp.target_bpm(140, bars=8, shape="ease_in_out") 1787 ``` 1788 """ 1789 1790 self._sequencer.set_target_bpm(bpm, bars, shape) 1791 1792 def live_info (self) -> typing.Dict[str, typing.Any]: 1793 1794 """ 1795 Return a dictionary containing the current state of the composition. 1796 1797 Includes BPM, key, current bar, active section, current chord, 1798 running patterns, and custom data. 1799 """ 1800 1801 section_info = None 1802 if self._form_state is not None: 1803 section = self._form_state.get_section_info() 1804 if section is not None: 1805 section_info = { 1806 "name": section.name, 1807 "bar": section.bar, 1808 "bars": section.bars, 1809 "progress": section.progress 1810 } 1811 1812 chord_name = None 1813 if self._harmonic_state is not None: 1814 chord = self._harmonic_state.get_current_chord() 1815 if chord is not None: 1816 chord_name = chord.name() 1817 1818 pattern_list = [] 1819 channel_offset = 0 if self._zero_indexed_channels else 1 1820 for name, pat in self._running_patterns.items(): 1821 pattern_list.append({ 1822 "name": name, 1823 "channel": pat.channel + channel_offset, 1824 "length": pat.length, 1825 "cycle": pat._cycle_count, 1826 "muted": pat._muted, 1827 "tweaks": dict(pat._tweaks) 1828 }) 1829 1830 return { 1831 "bpm": self._sequencer.current_bpm, 1832 "key": self.key, 1833 "bar": self._builder_bar, 1834 "section": section_info, 1835 "chord": chord_name, 1836 "patterns": pattern_list, 1837 "input_device": self._input_device, 1838 "clock_follow": self.is_clock_following, 1839 "data": self.data 1840 } 1841 1842 def mute (self, name: str) -> None: 1843 1844 """ 1845 Mute a running pattern by name. 1846 1847 The pattern continues to 'run' and increment its cycle count in 1848 the background, but it will not produce any MIDI notes until unmuted. 1849 1850 Parameters: 1851 name: The function name of the pattern to mute. 1852 """ 1853 1854 if name not in self._running_patterns: 1855 raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}") 1856 1857 self._running_patterns[name]._muted = True 1858 logger.info(f"Muted pattern: {name}") 1859 1860 def unmute (self, name: str) -> None: 1861 1862 """ 1863 Unmute a previously muted pattern. 1864 """ 1865 1866 if name not in self._running_patterns: 1867 raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}") 1868 1869 self._running_patterns[name]._muted = False 1870 logger.info(f"Unmuted pattern: {name}") 1871 1872 def tweak (self, name: str, **kwargs: typing.Any) -> None: 1873 1874 """Override parameters for a running pattern. 1875 1876 Values set here are available inside the pattern's builder 1877 function via ``p.param()``. They persist across rebuilds 1878 until explicitly changed or cleared. Changes take effect 1879 on the next rebuild cycle. 1880 1881 Parameters: 1882 name: The function name of the pattern. 1883 **kwargs: Parameter names and their new values. 1884 1885 Example (from the live REPL):: 1886 1887 composition.tweak("bass", pitches=[48, 52, 55, 60]) 1888 """ 1889 1890 if name not in self._running_patterns: 1891 raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}") 1892 1893 self._running_patterns[name]._tweaks.update(kwargs) 1894 logger.info(f"Tweaked pattern '{name}': {list(kwargs.keys())}") 1895 1896 def clear_tweak (self, name: str, *param_names: str) -> None: 1897 1898 """Remove tweaked parameters from a running pattern. 1899 1900 If no parameter names are given, all tweaks for the pattern 1901 are cleared and every ``p.param()`` call reverts to its 1902 default. 1903 1904 Parameters: 1905 name: The function name of the pattern. 1906 *param_names: Specific parameter names to clear. If 1907 omitted, all tweaks are removed. 1908 """ 1909 1910 if name not in self._running_patterns: 1911 raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}") 1912 1913 if not param_names: 1914 self._running_patterns[name]._tweaks.clear() 1915 logger.info(f"Cleared all tweaks for pattern '{name}'") 1916 else: 1917 for param_name in param_names: 1918 self._running_patterns[name]._tweaks.pop(param_name, None) 1919 logger.info(f"Cleared tweaks for pattern '{name}': {list(param_names)}") 1920 1921 def get_tweaks (self, name: str) -> typing.Dict[str, typing.Any]: 1922 1923 """Return a copy of the current tweaks for a running pattern. 1924 1925 Parameters: 1926 name: The function name of the pattern. 1927 """ 1928 1929 if name not in self._running_patterns: 1930 raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}") 1931 1932 return dict(self._running_patterns[name]._tweaks) 1933 1934 def schedule (self, fn: typing.Callable, cycle_beats: int, reschedule_lookahead: int = 1, wait_for_initial: bool = False, defer: bool = False) -> None: 1935 1936 """ 1937 Register a custom function to run on a repeating beat-based cycle. 1938 1939 Subsequence automatically runs synchronous functions in a thread pool 1940 so they don't block the timing-critical MIDI clock. Async functions 1941 are run directly on the event loop. 1942 1943 Parameters: 1944 fn: The function to call. 1945 cycle_beats: How often to call it (e.g., 4 = every bar). 1946 reschedule_lookahead: How far in advance to schedule the next call. 1947 wait_for_initial: If True, run the function once during startup 1948 and wait for it to complete before playback begins. This 1949 ensures ``composition.data`` is populated before patterns 1950 first build. Implies ``defer=True`` for the repeating 1951 schedule. 1952 defer: If True, skip the pulse-0 fire and defer the first 1953 repeating call to just before the second cycle boundary. 1954 """ 1955 1956 self._pending_scheduled.append(_PendingScheduled(fn, cycle_beats, reschedule_lookahead, wait_for_initial, defer)) 1957 1958 def form ( 1959 self, 1960 sections: typing.Union[ 1961 typing.List[typing.Tuple[str, int]], 1962 typing.Iterator[typing.Tuple[str, int]], 1963 typing.Dict[str, typing.Tuple[int, typing.Optional[typing.List[typing.Tuple[str, int]]]]] 1964 ], 1965 loop: bool = False, 1966 start: typing.Optional[str] = None 1967 ) -> None: 1968 1969 """ 1970 Define the structure (sections) of the composition. 1971 1972 You can define form in three ways: 1973 1. **Graph (Dict)**: Dynamic transitions based on weights. 1974 2. **Sequence (List)**: A fixed order of sections. 1975 3. **Generator**: A Python generator that yields `(name, bars)` pairs. 1976 1977 Parameters: 1978 sections: The form definition (Dict, List, or Generator). 1979 loop: Whether to cycle back to the start (List mode only). 1980 start: The section to start with (Graph mode only). 1981 1982 Example: 1983 ```python 1984 # A simple pop structure 1985 comp.form([ 1986 ("verse", 8), 1987 ("chorus", 8), 1988 ("verse", 8), 1989 ("chorus", 16) 1990 ]) 1991 ``` 1992 """ 1993 1994 self._form_state = subsequence.form_state.FormState(sections, loop=loop, start=start) 1995 1996 @staticmethod 1997 def _resolve_length ( 1998 beats: typing.Optional[float], 1999 bars: typing.Optional[float], 2000 steps: typing.Optional[float], 2001 unit: typing.Optional[float], 2002 default: float = 4.0 2003 ) -> typing.Tuple[float, int]: 2004 2005 """ 2006 Resolve the beat_length and default_grid from the duration parameters. 2007 2008 Two modes: 2009 - **Duration mode** (no ``unit``): specify ``beats=`` or ``bars=``. 2010 ``beats=4`` = 4 quarter notes; ``bars=2`` = 8 beats. 2011 - **Step mode** (with ``unit``): specify ``steps=`` and ``unit=``. 2012 ``steps=6, unit=dur.SIXTEENTH`` = 6 sixteenth notes = 1.5 beats. 2013 2014 Constraints: 2015 - ``beats`` and ``bars`` are mutually exclusive. 2016 - ``steps`` requires ``unit``; ``unit`` requires ``steps``. 2017 - ``steps`` cannot be combined with ``beats`` or ``bars``. 2018 2019 Returns: 2020 (beat_length, default_grid) — beat_length in beats (quarter notes), 2021 default_grid in 16th-note steps. 2022 """ 2023 2024 if beats is not None and bars is not None: 2025 raise ValueError("Specify only one of beats= or bars=") 2026 2027 if steps is not None and (beats is not None or bars is not None): 2028 raise ValueError("steps= cannot be combined with beats= or bars=") 2029 2030 if unit is not None and steps is None: 2031 raise ValueError("unit= requires steps= (e.g. steps=6, unit=dur.SIXTEENTH)") 2032 2033 if steps is not None: 2034 if unit is None: 2035 raise ValueError("steps= requires unit= (e.g. unit=dur.SIXTEENTH)") 2036 return steps * unit, int(steps) 2037 2038 if bars is not None: 2039 raw = bars * 4 2040 elif beats is not None: 2041 raw = beats 2042 else: 2043 raw = default 2044 2045 return raw, round(raw / subsequence.constants.durations.SIXTEENTH) 2046 2047 def pattern ( 2048 self, 2049 channel: int, 2050 beats: typing.Optional[float] = None, 2051 bars: typing.Optional[float] = None, 2052 steps: typing.Optional[float] = None, 2053 unit: typing.Optional[float] = None, 2054 drum_note_map: typing.Optional[typing.Dict[str, int]] = None, 2055 cc_name_map: typing.Optional[typing.Dict[str, int]] = None, 2056 reschedule_lookahead: float = 1, 2057 voice_leading: bool = False, 2058 device: subsequence.midi_utils.DeviceId = None, 2059 ) -> typing.Callable: 2060 2061 """ 2062 Register a function as a repeating MIDI pattern. 2063 2064 The decorated function will be called once per cycle to 'rebuild' its 2065 content. This allows for generative logic that evolves over time. 2066 2067 Two ways to specify pattern length: 2068 2069 - **Duration mode** (default): use ``beats=`` or ``bars=``. 2070 The grid defaults to sixteenth-note resolution. 2071 - **Step mode**: use ``steps=`` paired with ``unit=``. 2072 The grid equals the step count, so ``p.hit_steps()`` indices map 2073 directly to steps. 2074 2075 Parameters: 2076 channel: MIDI channel. By default uses 0-based numbering (0-15) 2077 matching the raw MIDI protocol. Set 2078 ``zero_indexed_channels=False`` on the ``Composition`` to use 2079 1-based numbering (1-16) instead. 2080 beats: Duration in beats (quarter notes). ``beats=4`` = 1 bar. 2081 bars: Duration in bars (4 beats each, assumes 4/4). ``bars=2`` = 8 beats. 2082 steps: Step count for step mode. Requires ``unit=``. 2083 unit: Duration of one step in beats (e.g. ``dur.SIXTEENTH``). 2084 Requires ``steps=``. 2085 drum_note_map: Optional mapping for drum instruments. 2086 cc_name_map: Optional mapping of CC names to MIDI CC numbers. 2087 Enables string-based CC names in ``p.cc()`` and ``p.cc_ramp()``. 2088 reschedule_lookahead: Beats in advance to compute the next cycle. 2089 voice_leading: If True, chords in this pattern will automatically 2090 use inversions that minimize voice movement. 2091 2092 Example: 2093 ```python 2094 @comp.pattern(channel=1, beats=4) 2095 def chords (p): 2096 p.chord([60, 64, 67], beat=0, velocity=80, duration=3.9) 2097 2098 @comp.pattern(channel=1, bars=2) 2099 def long_phrase (p): 2100 ... 2101 2102 @comp.pattern(channel=1, steps=6, unit=dur.SIXTEENTH) 2103 def riff (p): 2104 p.sequence(steps=[0, 1, 3, 5], pitches=60) 2105 ``` 2106 """ 2107 2108 channel = self._resolve_channel(channel) 2109 2110 beat_length, default_grid = self._resolve_length(beats, bars, steps, unit) 2111 2112 # Resolve device string name to index if possible now; otherwise store 2113 # the raw DeviceId and resolve it in _run() once all devices are open. 2114 resolved_device: subsequence.midi_utils.DeviceId = device 2115 2116 def decorator (fn: typing.Callable) -> typing.Callable: 2117 2118 """ 2119 Wrap the builder function and register it as a pending pattern. 2120 During live sessions, hot-swap an existing pattern's builder instead. 2121 """ 2122 2123 # Hot-swap: if we're live and a pattern with this name exists, replace its builder. 2124 if self._is_live and fn.__name__ in self._running_patterns: 2125 running = self._running_patterns[fn.__name__] 2126 running._builder_fn = fn 2127 running._wants_chord = _fn_has_parameter(fn, "chord") 2128 logger.info(f"Hot-swapped pattern: {fn.__name__}") 2129 return fn 2130 2131 pending = _PendingPattern( 2132 builder_fn = fn, 2133 channel = channel, # already resolved to 0-indexed 2134 length = beat_length, 2135 default_grid = default_grid, 2136 drum_note_map = drum_note_map, 2137 cc_name_map = cc_name_map, 2138 reschedule_lookahead = reschedule_lookahead, 2139 voice_leading = voice_leading, 2140 # For int/None: resolve immediately. For str: store 0 as 2141 # placeholder; _resolve_pending_devices() fixes it in _run(). 2142 device = 0 if (resolved_device is None or isinstance(resolved_device, str)) else resolved_device, 2143 raw_device = resolved_device, 2144 ) 2145 2146 self._pending_patterns.append(pending) 2147 2148 return fn 2149 2150 return decorator 2151 2152 def layer ( 2153 self, 2154 *builder_fns: typing.Callable, 2155 channel: int, 2156 beats: typing.Optional[float] = None, 2157 bars: typing.Optional[float] = None, 2158 steps: typing.Optional[float] = None, 2159 unit: typing.Optional[float] = None, 2160 drum_note_map: typing.Optional[typing.Dict[str, int]] = None, 2161 cc_name_map: typing.Optional[typing.Dict[str, int]] = None, 2162 reschedule_lookahead: float = 1, 2163 voice_leading: bool = False, 2164 device: subsequence.midi_utils.DeviceId = None, 2165 ) -> None: 2166 2167 """ 2168 Combine multiple functions into a single MIDI pattern. 2169 2170 This is useful for composing complex patterns out of reusable 2171 building blocks (e.g., a 'kick' function and a 'snare' function). 2172 2173 See ``pattern()`` for the full description of ``beats``, ``bars``, 2174 ``steps``, and ``unit``. 2175 2176 Parameters: 2177 builder_fns: One or more pattern builder functions. 2178 channel: MIDI channel (0-15, or 1-16 with ``zero_indexed_channels=False``). 2179 beats: Duration in beats (quarter notes). 2180 bars: Duration in bars (4 beats each, assumes 4/4). 2181 steps: Step count for step mode. Requires ``unit=``. 2182 unit: Duration of one step in beats. Requires ``steps=``. 2183 drum_note_map: Optional mapping for drum instruments. 2184 cc_name_map: Optional mapping of CC names to MIDI CC numbers. 2185 reschedule_lookahead: Beats in advance to compute the next cycle. 2186 voice_leading: If True, chords use smooth voice leading. 2187 """ 2188 2189 beat_length, default_grid = self._resolve_length(beats, bars, steps, unit) 2190 2191 wants_chord = any(_fn_has_parameter(fn, "chord") for fn in builder_fns) 2192 2193 if wants_chord: 2194 2195 def merged_builder (p: subsequence.pattern_builder.PatternBuilder, chord: _InjectedChord) -> None: 2196 2197 for fn in builder_fns: 2198 if _fn_has_parameter(fn, "chord"): 2199 fn(p, chord) 2200 else: 2201 fn(p) 2202 2203 else: 2204 2205 def merged_builder (p: subsequence.pattern_builder.PatternBuilder) -> None: # type: ignore[misc] 2206 2207 for fn in builder_fns: 2208 fn(p) 2209 2210 resolved = self._resolve_channel(channel) 2211 2212 pending = _PendingPattern( 2213 builder_fn = merged_builder, 2214 channel = resolved, # already resolved to 0-indexed 2215 length = beat_length, 2216 default_grid = default_grid, 2217 drum_note_map = drum_note_map, 2218 cc_name_map = cc_name_map, 2219 reschedule_lookahead = reschedule_lookahead, 2220 voice_leading = voice_leading, 2221 device = 0 if (device is None or isinstance(device, str)) else device, 2222 raw_device = device, 2223 ) 2224 2225 self._pending_patterns.append(pending) 2226 2227 def trigger ( 2228 self, 2229 fn: typing.Callable, 2230 channel: int, 2231 beats: typing.Optional[float] = None, 2232 bars: typing.Optional[float] = None, 2233 steps: typing.Optional[float] = None, 2234 unit: typing.Optional[float] = None, 2235 quantize: float = 0, 2236 drum_note_map: typing.Optional[typing.Dict[str, int]] = None, 2237 cc_name_map: typing.Optional[typing.Dict[str, int]] = None, 2238 chord: bool = False, 2239 device: subsequence.midi_utils.DeviceId = None, 2240 ) -> None: 2241 2242 """ 2243 Trigger a one-shot pattern immediately or on a quantized boundary. 2244 2245 This is useful for real-time response to sensors, OSC messages, or other 2246 external events. The builder function is called immediately with a fresh 2247 PatternBuilder, and the generated events are injected into the queue at 2248 the specified quantize boundary. 2249 2250 The builder function has the same API as a ``@composition.pattern`` 2251 decorated function and can use all PatternBuilder methods: ``p.note()``, 2252 ``p.euclidean()``, ``p.arpeggio()``, and so on. 2253 2254 See ``pattern()`` for the full description of ``beats``, ``bars``, 2255 ``steps``, and ``unit``. Default is 1 beat. 2256 2257 Parameters: 2258 fn: The pattern builder function (same signature as ``@comp.pattern``). 2259 channel: MIDI channel (0-15, or 1-16 with ``zero_indexed_channels=False``). 2260 beats: Duration in beats (quarter notes, default 1). 2261 bars: Duration in bars (4 beats each, assumes 4/4). 2262 steps: Step count for step mode. Requires ``unit=``. 2263 unit: Duration of one step in beats. Requires ``steps=``. 2264 quantize: Snap the trigger to a beat boundary: ``0`` = immediate (default), 2265 ``1`` = next beat (quarter note), ``4`` = next bar. Use ``dur.*`` 2266 constants from ``subsequence.constants.durations``. 2267 drum_note_map: Optional drum name mapping for this pattern. 2268 cc_name_map: Optional mapping of CC names to MIDI CC numbers. 2269 chord: If ``True``, the builder function receives the current chord as 2270 a second parameter (same as ``@composition.pattern``). 2271 2272 Example: 2273 ```python 2274 # Immediate single note 2275 composition.trigger( 2276 lambda p: p.note(60, beat=0, velocity=100, duration=0.5), 2277 channel=0 2278 ) 2279 2280 # Quantized fill (next bar) 2281 import subsequence.constants.durations as dur 2282 composition.trigger( 2283 lambda p: p.euclidean("snare", pulses=7, velocity=90), 2284 channel=9, 2285 drum_note_map=gm_drums.GM_DRUM_MAP, 2286 quantize=dur.WHOLE 2287 ) 2288 2289 # With chord context 2290 composition.trigger( 2291 lambda p: p.arpeggio(p.chord.tones(root=60), spacing=dur.SIXTEENTH), 2292 channel=0, 2293 quantize=dur.QUARTER, 2294 chord=True 2295 ) 2296 ``` 2297 """ 2298 2299 # Resolve channel numbering 2300 resolved_channel = self._resolve_channel(channel) 2301 2302 beat_length, default_grid = self._resolve_length(beats, bars, steps, unit, default=1.0) 2303 2304 # Resolve device index 2305 resolved_device_idx = self._resolve_device_id(device) 2306 2307 # Create a temporary Pattern 2308 pattern = subsequence.pattern.Pattern(channel=resolved_channel, length=beat_length, device=resolved_device_idx) 2309 2310 # Create a PatternBuilder 2311 builder = subsequence.pattern_builder.PatternBuilder( 2312 pattern=pattern, 2313 cycle=0, # One-shot patterns don't rebuild, so cycle is always 0 2314 drum_note_map=drum_note_map, 2315 cc_name_map=cc_name_map, 2316 section=self._form_state.get_section_info() if self._form_state else None, 2317 bar=self._builder_bar, 2318 conductor=self.conductor, 2319 rng=random.Random(), # Fresh random state for each trigger 2320 tweaks={}, 2321 default_grid=default_grid, 2322 data=self.data 2323 ) 2324 2325 # Call the builder function 2326 try: 2327 2328 if chord and self._harmonic_state is not None: 2329 current_chord = self._harmonic_state.get_current_chord() 2330 injected = _InjectedChord(current_chord, None) # No voice leading for one-shots 2331 fn(builder, injected) 2332 2333 else: 2334 fn(builder) 2335 2336 except Exception: 2337 logger.exception("Error in trigger builder — pattern will be silent") 2338 return 2339 2340 # Calculate the start pulse based on quantize 2341 current_pulse = self._sequencer.pulse_count 2342 pulses_per_beat = subsequence.constants.MIDI_QUARTER_NOTE 2343 2344 if quantize == 0: 2345 # Immediate: use current pulse 2346 start_pulse = current_pulse 2347 2348 else: 2349 # Quantize to the next multiple of (quantize * pulses_per_beat) 2350 quantize_pulses = int(quantize * pulses_per_beat) 2351 start_pulse = ((current_pulse // quantize_pulses) + 1) * quantize_pulses 2352 2353 # Schedule the pattern for one-shot execution 2354 try: 2355 loop = asyncio.get_running_loop() 2356 # Already on the event loop 2357 asyncio.create_task(self._sequencer.schedule_pattern(pattern, start_pulse)) 2358 2359 except RuntimeError: 2360 # Not on the event loop — schedule via call_soon_threadsafe 2361 if self._sequencer._event_loop is not None: 2362 asyncio.run_coroutine_threadsafe( 2363 self._sequencer.schedule_pattern(pattern, start_pulse), 2364 loop=self._sequencer._event_loop 2365 ) 2366 else: 2367 logger.warning("trigger() called before playback started; pattern ignored") 2368 2369 @property 2370 def is_clock_following (self) -> bool: 2371 2372 """True if either the primary or any additional device is following external clock.""" 2373 2374 return self._clock_follow or any(cf for _, _, cf in self._additional_inputs) 2375 2376 2377 def play (self) -> None: 2378 2379 """ 2380 Start the composition. 2381 2382 This call blocks until the program is interrupted (e.g., via Ctrl+C). 2383 It initializes the MIDI hardware, launches the background sequencer, 2384 and begins playback. 2385 """ 2386 2387 try: 2388 asyncio.run(self._run()) 2389 2390 except KeyboardInterrupt: 2391 pass 2392 2393 2394 def render (self, bars: typing.Optional[int] = None, filename: str = "render.mid", max_minutes: typing.Optional[float] = 60.0) -> None: 2395 2396 """Render the composition to a MIDI file without real-time playback. 2397 2398 Runs the sequencer as fast as possible (no timing delays) and stops 2399 when the first active limit is reached. The result is saved as a 2400 standard MIDI file that can be imported into any DAW. 2401 2402 All patterns, scheduled callbacks, and harmony logic run exactly as 2403 they would during live playback — BPM transitions, generative fills, 2404 and probabilistic gates all work in render mode. The only difference 2405 is that time is simulated rather than wall-clock driven. 2406 2407 Parameters: 2408 bars: Number of bars to render, or ``None`` for no bar limit 2409 (default ``None``). When both *bars* and *max_minutes* are 2410 active, playback stops at whichever limit is reached first. 2411 filename: Output MIDI filename (default ``"render.mid"``). 2412 max_minutes: Safety cap on the length of rendered MIDI in minutes 2413 (default ``60.0``). Pass ``None`` to disable the time 2414 cap — you must then provide an explicit *bars* value. 2415 2416 Raises: 2417 ValueError: If both *bars* and *max_minutes* are ``None``, which 2418 would produce an infinite render. 2419 2420 Examples: 2421 ```python 2422 # Default: renders up to 60 minutes of MIDI content. 2423 composition.render() 2424 2425 # Render exactly 64 bars (time cap still active as backstop). 2426 composition.render(bars=64, filename="demo.mid") 2427 2428 # Render up to 5 minutes of an infinite generative composition. 2429 composition.render(max_minutes=5, filename="five_min.mid") 2430 2431 # Remove the time cap — must supply bars instead. 2432 composition.render(bars=128, max_minutes=None, filename="long.mid") 2433 ``` 2434 """ 2435 2436 if bars is None and max_minutes is None: 2437 raise ValueError( 2438 "render() requires at least one limit: provide bars=, max_minutes=, or both. " 2439 "Passing both as None would produce an infinite render." 2440 ) 2441 2442 self._sequencer.recording = True 2443 self._sequencer.record_filename = filename 2444 self._sequencer.render_mode = True 2445 self._sequencer.render_bars = bars if bars is not None else 0 2446 self._sequencer.render_max_seconds = max_minutes * 60.0 if max_minutes is not None else None 2447 asyncio.run(self._run()) 2448 2449 async def _run (self) -> None: 2450 2451 """ 2452 Async entry point that schedules all patterns and runs the sequencer. 2453 """ 2454 2455 # 1. Pre-calculate MIDI input indices and configure sequencer clock follow. 2456 if self._input_device is not None: 2457 self._sequencer.input_device_name = self._input_device 2458 self._sequencer.clock_follow = self._clock_follow 2459 self._sequencer.clock_device_idx = 0 2460 2461 if not self._clock_follow: 2462 # Find first additional input that wants to be the clock master. 2463 for idx, (_, _, cf) in enumerate(self._additional_inputs, start=1): 2464 if cf: 2465 self._sequencer.clock_follow = True 2466 self._sequencer.clock_device_idx = idx 2467 break 2468 2469 # Populate input device name mapping early (before opening ports) so we can 2470 # resolve CC mappings to integer device indices immediately. 2471 if self._sequencer.input_device_name: 2472 self._input_device_names[self._sequencer.input_device_name] = 0 2473 if self._input_device_alias is not None: 2474 self._input_device_names[self._input_device_alias] = 0 2475 2476 for idx, (dev_name, alias, _) in enumerate(self._additional_inputs, start=1): 2477 self._input_device_names[dev_name] = idx 2478 if alias: 2479 self._input_device_names[alias] = idx 2480 2481 # 2. Pre-calculate output device names. 2482 if self._sequencer.output_device_name: 2483 self._output_device_names[self._sequencer.output_device_name] = 0 2484 2485 # 3. Resolve name-based device ids in cc_map/cc_forward/pending patterns early. 2486 # This ensures we have integer indices ready for the background callback thread. 2487 self._resolve_pending_devices() 2488 for mapping in self._cc_mappings: 2489 raw = mapping.get('input_device') 2490 if isinstance(raw, str): 2491 mapping['input_device'] = self._resolve_input_device_id(raw) 2492 for fwd in self._cc_forwards: 2493 raw_in = fwd.get('input_device') 2494 if isinstance(raw_in, str): 2495 fwd['input_device'] = self._resolve_input_device_id(raw_in) 2496 raw_out = fwd.get('output_device') 2497 if isinstance(raw_out, str): 2498 fwd['output_device'] = self._resolve_device_id(raw_out) 2499 2500 # 4. Share CC input mappings, forwards, and a reference to composition.data 2501 # with the sequencer BEFORE opening the ports. This ensures that any initial 2502 # messages in the OS buffer are correctly mapped as soon as the port opens. 2503 self._sequencer.cc_mappings = self._cc_mappings 2504 self._sequencer.cc_forwards = self._cc_forwards 2505 self._sequencer._composition_data = self.data 2506 2507 # 5. Open MIDI input ports early. Even without a deliberate sleep, opening 2508 # them before pattern building minimizes the window for missed messages. 2509 # Primary input 2510 self._sequencer._open_midi_inputs() 2511 2512 # Additional inputs 2513 for idx, (dev_name, alias, cf) in enumerate(self._additional_inputs, start=1): 2514 # Use the pre-calculated index 2515 callback = self._sequencer._make_input_callback(idx) 2516 open_name, port = subsequence.midi_utils.select_input_device(dev_name, callback) 2517 if open_name and port is not None: 2518 self._sequencer.add_input_device(open_name, port) 2519 else: 2520 logger.warning(f"Could not open additional input device '{dev_name}'") 2521 2522 # 6. Open additional MIDI output devices. 2523 for dev_name, alias in self._additional_outputs: 2524 open_name, port = subsequence.midi_utils.select_output_device(dev_name) 2525 if open_name and port is not None: 2526 idx = self._sequencer.add_output_device(open_name, port) 2527 self._output_device_names[open_name] = idx 2528 if alias is not None: 2529 self._output_device_names[alias] = idx 2530 else: 2531 logger.warning(f"Could not open additional output device '{dev_name}'") 2532 2533 # Resolve any name-based output device IDs on patterns that may have been added 2534 # for additional output devices. 2535 self._resolve_pending_devices() 2536 2537 # Pass clock output flag (suppressed automatically when clock_follow=True). 2538 self._sequencer.clock_output = self._clock_output and not self.is_clock_following 2539 2540 # Create Ableton Link clock if comp.link() was called. 2541 if self._link_quantum is not None: 2542 self._sequencer._link_clock = subsequence.link_clock.LinkClock( 2543 bpm = self.bpm, 2544 quantum = self._link_quantum, 2545 loop = asyncio.get_running_loop(), 2546 ) 2547 2548 # Derive child RNGs from the master seed so each component gets 2549 # an independent, deterministic stream. When no seed is set, 2550 # each component creates its own unseeded RNG (existing behaviour). 2551 self._pattern_rngs: typing.List[random.Random] = [] 2552 2553 if self._seed is not None: 2554 master = random.Random(self._seed) 2555 2556 if self._harmonic_state is not None: 2557 self._harmonic_state.rng = random.Random(master.randint(0, 2 ** 63)) 2558 2559 if self._form_state is not None: 2560 self._form_state._rng = random.Random(master.randint(0, 2 ** 63)) 2561 2562 for _ in self._pending_patterns: 2563 self._pattern_rngs.append(random.Random(master.randint(0, 2 ** 63))) 2564 2565 if self._harmonic_state is not None and self._harmony_cycle_beats is not None: 2566 2567 def _get_section_progression () -> typing.Optional[typing.Tuple[str, int, typing.Optional[Progression]]]: 2568 """Return (section_name, section_index, Progression|None) for the current section, or None.""" 2569 if self._form_state is None: 2570 return None 2571 info = self._form_state.get_section_info() 2572 if info is None: 2573 return None 2574 prog = self._section_progressions.get(info.name) 2575 return (info.name, info.index, prog) 2576 2577 await schedule_harmonic_clock( 2578 sequencer = self._sequencer, 2579 get_harmonic_state = lambda: self._harmonic_state, 2580 cycle_beats = self._harmony_cycle_beats, 2581 reschedule_lookahead = self._harmony_reschedule_lookahead, 2582 get_section_progression = _get_section_progression, 2583 ) 2584 2585 if self._form_state is not None: 2586 2587 await schedule_form( 2588 sequencer = self._sequencer, 2589 form_state = self._form_state, 2590 reschedule_lookahead = 1 2591 ) 2592 2593 # Bar counter - always active so p.bar is available to all builders. 2594 def _advance_builder_bar (pulse: int) -> None: 2595 self._builder_bar += 1 2596 2597 first_bar_pulse = int(self.time_signature[0] * self._sequencer.pulses_per_beat) 2598 2599 await self._sequencer.schedule_callback_repeating( 2600 callback = _advance_builder_bar, 2601 interval_beats = self.time_signature[0], 2602 start_pulse = first_bar_pulse, 2603 reschedule_lookahead = 1 2604 ) 2605 2606 # Run wait_for_initial=True scheduled functions and block until all complete. 2607 # This ensures composition.data is populated before patterns build. 2608 initial_tasks = [t for t in self._pending_scheduled if t.wait_for_initial] 2609 2610 if initial_tasks: 2611 2612 names = ", ".join(t.fn.__name__ for t in initial_tasks) 2613 logger.info(f"Waiting for initial scheduled {'function' if len(initial_tasks) == 1 else 'functions'} before start: {names}") 2614 2615 async def _run_initial (fn: typing.Callable) -> None: 2616 2617 accepts_ctx = _fn_has_parameter(fn, "p") 2618 ctx = ScheduleContext(cycle=0) 2619 2620 try: 2621 if asyncio.iscoroutinefunction(fn): 2622 await (fn(ctx) if accepts_ctx else fn()) 2623 else: 2624 loop = asyncio.get_running_loop() 2625 call = (lambda: fn(ctx)) if accepts_ctx else fn 2626 await loop.run_in_executor(None, call) 2627 except Exception as exc: 2628 logger.warning(f"Initial run of {fn.__name__!r} failed: {exc}") 2629 2630 await asyncio.gather(*[_run_initial(t.fn) for t in initial_tasks]) 2631 2632 for pending_task in self._pending_scheduled: 2633 2634 accepts_ctx = _fn_has_parameter(pending_task.fn, "p") 2635 wrapped = _make_safe_callback(pending_task.fn, accepts_context=accepts_ctx) 2636 2637 # wait_for_initial=True implies defer — no point firing at pulse 0 2638 # after the blocking run just completed. defer=True skips the 2639 # backshift fire so the first repeating call happens one full cycle 2640 # later. 2641 if pending_task.wait_for_initial or pending_task.defer: 2642 start_pulse = int(pending_task.cycle_beats * self._sequencer.pulses_per_beat) 2643 else: 2644 start_pulse = 0 2645 2646 await self._sequencer.schedule_callback_repeating( 2647 callback = wrapped, 2648 interval_beats = pending_task.cycle_beats, 2649 start_pulse = start_pulse, 2650 reschedule_lookahead = pending_task.reschedule_lookahead 2651 ) 2652 2653 # Build Pattern objects from pending registrations. 2654 patterns: typing.List[subsequence.pattern.Pattern] = [] 2655 2656 for i, pending in enumerate(self._pending_patterns): 2657 2658 pattern_rng = self._pattern_rngs[i] if i < len(self._pattern_rngs) else None 2659 pattern = self._build_pattern_from_pending(pending, pattern_rng) 2660 patterns.append(pattern) 2661 2662 await schedule_patterns( 2663 sequencer = self._sequencer, 2664 patterns = patterns, 2665 start_pulse = 0 2666 ) 2667 2668 # Populate the running patterns dict for live hot-swap and mute/unmute. 2669 for i, pending in enumerate(self._pending_patterns): 2670 name = pending.builder_fn.__name__ 2671 self._running_patterns[name] = patterns[i] 2672 2673 if self._display is not None and not self._sequencer.render_mode: 2674 self._display.start() 2675 self._sequencer.on_event("bar", self._display.update) 2676 self._sequencer.on_event("beat", self._display.update) 2677 2678 if self._live_server is not None: 2679 await self._live_server.start() 2680 2681 if self._osc_server is not None: 2682 await self._osc_server.start() 2683 self._sequencer.osc_server = self._osc_server 2684 2685 def _send_osc_status (bar: int) -> None: 2686 if self._osc_server: 2687 self._osc_server.send("/bar", bar) 2688 self._osc_server.send("/bpm", self._sequencer.current_bpm) 2689 2690 if self._harmonic_state: 2691 self._osc_server.send("/chord", self._harmonic_state.current_chord.name()) 2692 2693 if self._form_state: 2694 info = self._form_state.get_section_info() 2695 if info: 2696 self._osc_server.send("/section", info.name) 2697 2698 self._sequencer.on_event("bar", _send_osc_status) 2699 2700 # Start keystroke listener if hotkeys are enabled and not in render mode. 2701 if self._hotkeys_enabled and not self._sequencer.render_mode: 2702 self._keystroke_listener = subsequence.keystroke.KeystrokeListener() 2703 self._keystroke_listener.start() 2704 2705 if self._keystroke_listener.active: 2706 # Listener started successfully — register the bar handler 2707 # and show all bindings so the user knows what's available. 2708 self._sequencer.on_event("bar", self._process_hotkeys) 2709 self._list_hotkeys() 2710 # If not active, KeystrokeListener.start() already logged a warning. 2711 2712 if self._web_ui_enabled and not self._sequencer.render_mode: 2713 self._web_ui_server = subsequence.web_ui.WebUI(self) 2714 self._web_ui_server.start() 2715 2716 await run_until_stopped(self._sequencer) 2717 2718 if self._web_ui_server is not None: 2719 self._web_ui_server.stop() 2720 2721 if self._live_server is not None: 2722 await self._live_server.stop() 2723 2724 if self._osc_server is not None: 2725 await self._osc_server.stop() 2726 self._sequencer.osc_server = None 2727 2728 if self._display is not None: 2729 self._display.stop() 2730 2731 if self._keystroke_listener is not None: 2732 self._keystroke_listener.stop() 2733 self._keystroke_listener = None 2734 2735 def _build_pattern_from_pending (self, pending: _PendingPattern, rng: typing.Optional[random.Random] = None) -> subsequence.pattern.Pattern: 2736 2737 """ 2738 Create a Pattern from a pending registration using a temporary subclass. 2739 """ 2740 2741 composition_ref = self 2742 2743 class _DecoratorPattern (subsequence.pattern.Pattern): 2744 2745 """ 2746 Pattern subclass that delegates to a builder function on each reschedule. 2747 """ 2748 2749 def __init__ (self, pending: _PendingPattern, pattern_rng: typing.Optional[random.Random] = None) -> None: 2750 2751 """ 2752 Initialize the decorator pattern from pending registration details. 2753 """ 2754 2755 super().__init__( 2756 channel = pending.channel, 2757 length = pending.length, 2758 reschedule_lookahead = min( 2759 pending.reschedule_lookahead, 2760 composition_ref._harmony_reschedule_lookahead 2761 ), 2762 device = pending.device, 2763 ) 2764 2765 self._builder_fn = pending.builder_fn 2766 self._drum_note_map = pending.drum_note_map 2767 self._cc_name_map = pending.cc_name_map 2768 self._default_grid: int = pending.default_grid 2769 self._wants_chord = _fn_has_parameter(pending.builder_fn, "chord") 2770 self._cycle_count = 0 2771 self._rng = pattern_rng 2772 self._muted = False 2773 self._voice_leading_state: typing.Optional[subsequence.voicings.VoiceLeadingState] = ( 2774 subsequence.voicings.VoiceLeadingState() if pending.voice_leading else None 2775 ) 2776 self._tweaks: typing.Dict[str, typing.Any] = {} 2777 2778 self._rebuild() 2779 2780 def _rebuild (self) -> None: 2781 2782 """ 2783 Clear steps and call the builder function to repopulate. 2784 """ 2785 2786 self.steps = {} 2787 self.cc_events = [] 2788 self.osc_events = [] 2789 current_cycle = self._cycle_count 2790 self._cycle_count += 1 2791 2792 if self._muted: 2793 return 2794 2795 builder = subsequence.pattern_builder.PatternBuilder( 2796 pattern = self, 2797 cycle = current_cycle, 2798 drum_note_map = self._drum_note_map, 2799 cc_name_map = self._cc_name_map, 2800 section = composition_ref._form_state.get_section_info() if composition_ref._form_state else None, 2801 bar = composition_ref._builder_bar, 2802 conductor = composition_ref.conductor, 2803 rng = self._rng, 2804 tweaks = self._tweaks, 2805 default_grid = self._default_grid, 2806 data = composition_ref.data 2807 ) 2808 2809 try: 2810 2811 if self._wants_chord and composition_ref._harmonic_state is not None: 2812 chord = composition_ref._harmonic_state.get_current_chord() 2813 injected = _InjectedChord(chord, self._voice_leading_state) 2814 self._builder_fn(builder, injected) 2815 2816 else: 2817 self._builder_fn(builder) 2818 2819 except Exception: 2820 logger.exception("Error in pattern builder '%s' (cycle %d) - pattern will be silent this cycle", self._builder_fn.__name__, current_cycle) 2821 2822 # Auto-apply global tuning if set and not already applied by the builder. 2823 if ( 2824 composition_ref._tuning is not None 2825 and not builder._tuning_applied 2826 and not (composition_ref._tuning_exclude_drums and self._drum_note_map) 2827 ): 2828 import subsequence.tuning as _tuning_mod 2829 _tuning_mod.apply_tuning_to_pattern( 2830 self, 2831 composition_ref._tuning, 2832 bend_range=composition_ref._tuning_bend_range, 2833 channels=composition_ref._tuning_channels, 2834 reference_note=composition_ref._tuning_reference_note, 2835 ) 2836 2837 def on_reschedule (self) -> None: 2838 2839 """ 2840 Rebuild the pattern from the builder function before the next cycle. 2841 """ 2842 2843 self._rebuild() 2844 2845 return _DecoratorPattern(pending, rng)
The top-level controller for a musical piece.
The Composition object manages the global clock (Sequencer), the harmonic
progression (HarmonicState), the song structure (subsequence.form_state.FormState), and all MIDI patterns.
It serves as the main entry point for defining your music.
Typical workflow:
- Initialize
Compositionwith BPM and Key. - Define harmony and form (optional).
- Register patterns using the
@composition.patterndecorator. - Call
composition.play()to start the music.
581 def __init__ ( 582 self, 583 output_device: typing.Optional[str] = None, 584 bpm: float = 120, 585 time_signature: typing.Tuple[int, int] = (4, 4), 586 key: typing.Optional[str] = None, 587 seed: typing.Optional[int] = None, 588 record: bool = False, 589 record_filename: typing.Optional[str] = None, 590 zero_indexed_channels: bool = False 591 ) -> None: 592 593 """ 594 Initialize a new composition. 595 596 Parameters: 597 output_device: The name of the MIDI output port to use. If `None`, 598 Subsequence will attempt to find a device, prompting if necessary. 599 bpm: Initial tempo in beats per minute (default 120). 600 key: The root key of the piece (e.g., "C", "F#", "Bb"). 601 Required if you plan to use `harmony()`. 602 seed: An optional integer for deterministic randomness. When set, 603 every random decision (chord choices, drum probability, etc.) 604 will be identical on every run. 605 record: When True, record all MIDI events to a file. 606 record_filename: Optional filename for the recording (defaults to timestamp). 607 zero_indexed_channels: When False (default), MIDI channels use 608 1-based numbering (1-16) matching instrument labelling. 609 Channel 10 is drums, the way musicians and hardware panels 610 show it. When True, channels use 0-based numbering (0-15) 611 matching the raw MIDI protocol. 612 613 Example: 614 ```python 615 comp = subsequence.Composition(bpm=128, key="Eb", seed=123) 616 ``` 617 """ 618 619 self.output_device = output_device 620 self.bpm = bpm 621 self.time_signature = time_signature 622 self.key = key 623 self._seed: typing.Optional[int] = seed 624 self._zero_indexed_channels: bool = zero_indexed_channels 625 626 self._sequencer = subsequence.sequencer.Sequencer( 627 output_device_name = output_device, 628 initial_bpm = bpm, 629 time_signature = time_signature, 630 record = record, 631 record_filename = record_filename 632 ) 633 634 self._harmonic_state: typing.Optional[subsequence.harmonic_state.HarmonicState] = None 635 self._harmony_cycle_beats: typing.Optional[int] = None 636 self._harmony_reschedule_lookahead: float = 1 637 self._section_progressions: typing.Dict[str, Progression] = {} 638 self._pending_patterns: typing.List[_PendingPattern] = [] 639 self._pending_scheduled: typing.List[_PendingScheduled] = [] 640 self._form_state: typing.Optional[subsequence.form_state.FormState] = None 641 self._builder_bar: int = 0 642 self._display: typing.Optional[subsequence.display.Display] = None 643 self._live_server: typing.Optional[subsequence.live_server.LiveServer] = None 644 self._is_live: bool = False 645 self._running_patterns: typing.Dict[str, typing.Any] = {} 646 self._input_device: typing.Optional[str] = None 647 self._input_device_alias: typing.Optional[str] = None 648 self._clock_follow: bool = False 649 self._clock_output: bool = False 650 self._cc_mappings: typing.List[typing.Dict[str, typing.Any]] = [] 651 self._cc_forwards: typing.List[typing.Dict[str, typing.Any]] = [] 652 # Additional output devices registered with midi_output() after construction. 653 # Each entry: (device_name: str, alias: Optional[str]) 654 self._additional_outputs: typing.List[typing.Tuple[str, typing.Optional[str]]] = [] 655 # Additional input devices: (device_name: str, alias: Optional[str], clock_follow: bool) 656 self._additional_inputs: typing.List[typing.Tuple[str, typing.Optional[str], bool]] = [] 657 # Maps alias/name → output device index (populated in _run after all devices are opened). 658 self._output_device_names: typing.Dict[str, int] = {} 659 # Maps alias/name → input device index (populated in _run after all input devices are opened). 660 self._input_device_names: typing.Dict[str, int] = {} 661 self.data: typing.Dict[str, typing.Any] = {} 662 self._osc_server: typing.Optional[subsequence.osc.OscServer] = None 663 self.conductor = subsequence.conductor.Conductor() 664 self._web_ui_enabled: bool = False 665 self._web_ui_server: typing.Optional[subsequence.web_ui.WebUI] = None 666 self._link_quantum: typing.Optional[float] = None 667 668 # Hotkey state — populated by hotkeys() and hotkey(). 669 self._hotkeys_enabled: bool = False 670 self._hotkey_bindings: typing.Dict[str, HotkeyBinding] = {} 671 self._pending_hotkey_actions: typing.List[_PendingHotkeyAction] = [] 672 self._keystroke_listener: typing.Optional[subsequence.keystroke.KeystrokeListener] = None 673 674 # Tuning state — populated by tuning(). 675 self._tuning: typing.Optional[typing.Any] = None # subsequence.tuning.Tuning 676 self._tuning_bend_range: float = 2.0 677 self._tuning_channels: typing.Optional[typing.List[int]] = None 678 self._tuning_reference_note: int = 60 679 self._tuning_exclude_drums: bool = True
Initialize a new composition.
Arguments:
- output_device: The name of the MIDI output port to use. If
None, Subsequence will attempt to find a device, prompting if necessary. - bpm: Initial tempo in beats per minute (default 120).
- key: The root key of the piece (e.g., "C", "F#", "Bb").
Required if you plan to use
harmony(). - seed: An optional integer for deterministic randomness. When set, every random decision (chord choices, drum probability, etc.) will be identical on every run.
- record: When True, record all MIDI events to a file.
- record_filename: Optional filename for the recording (defaults to timestamp).
- zero_indexed_channels: When False (default), MIDI channels use 1-based numbering (1-16) matching instrument labelling. Channel 10 is drums, the way musicians and hardware panels show it. When True, channels use 0-based numbering (0-15) matching the raw MIDI protocol.
Example:
comp = subsequence.Composition(bpm=128, key="Eb", seed=123)
748 @property 749 def harmonic_state (self) -> typing.Optional[subsequence.harmonic_state.HarmonicState]: 750 """The active ``HarmonicState``, or ``None`` if ``harmony()`` has not been called.""" 751 return self._harmonic_state
The active HarmonicState, or None if harmony() has not been called.
753 @property 754 def form_state (self) -> typing.Optional["subsequence.form_state.FormState"]: 755 """The active ``subsequence.form_state.FormState``, or ``None`` if ``form()`` has not been called.""" 756 return self._form_state
The active subsequence.form_state.FormState, or None if form() has not been called.
758 @property 759 def sequencer (self) -> subsequence.sequencer.Sequencer: 760 """The underlying ``Sequencer`` instance.""" 761 return self._sequencer
The underlying Sequencer instance.
763 @property 764 def running_patterns (self) -> typing.Dict[str, typing.Any]: 765 """The currently active patterns, keyed by name.""" 766 return self._running_patterns
The currently active patterns, keyed by name.
768 @property 769 def builder_bar (self) -> int: 770 """Current bar index used by pattern builders.""" 771 return self._builder_bar
Current bar index used by pattern builders.
782 def harmony ( 783 self, 784 style: typing.Union[str, subsequence.chord_graphs.ChordGraph] = "functional_major", 785 cycle_beats: int = 4, 786 dominant_7th: bool = True, 787 gravity: float = 1.0, 788 nir_strength: float = 0.5, 789 minor_turnaround_weight: float = 0.0, 790 root_diversity: float = subsequence.harmonic_state.DEFAULT_ROOT_DIVERSITY, 791 reschedule_lookahead: float = 1 792 ) -> None: 793 794 """ 795 Configure the harmonic logic and chord change intervals. 796 797 Subsequence uses a weighted transition graph to choose the next chord. 798 You can influence these choices using 'gravity' (favoring the tonic) and 799 'NIR strength' (melodic inertia based on Narmour's model). 800 801 Parameters: 802 style: The harmonic style to use. Built-in: "functional_major" 803 (alias "diatonic_major"), "turnaround", "aeolian_minor", 804 "phrygian_minor", "lydian_major", "dorian_minor", 805 "chromatic_mediant", "suspended", "mixolydian", "whole_tone", 806 "diminished". See README for full descriptions. 807 cycle_beats: How many beats each chord lasts (default 4). 808 dominant_7th: Whether to include V7 chords (default True). 809 gravity: Key gravity (0.0 to 1.0). High values stay closer to the root chord. 810 nir_strength: Melodic inertia (0.0 to 1.0). Influences chord movement 811 expectations. 812 minor_turnaround_weight: For "turnaround" style, influences major vs minor feel. 813 root_diversity: Root-repetition damping (0.0 to 1.0). Each recent 814 chord sharing a candidate's root reduces the weight to 40% at 815 the default (0.4). Set to 1.0 to disable. 816 reschedule_lookahead: How many beats in advance to calculate the 817 next chord. 818 819 Example: 820 ```python 821 # A moody minor progression that changes every 8 beats 822 comp.harmony(style="aeolian_minor", cycle_beats=8, gravity=0.4) 823 ``` 824 """ 825 826 if self.key is None: 827 raise ValueError("Cannot configure harmony without a key - set key in the Composition constructor") 828 829 preserved_history: typing.List[subsequence.chords.Chord] = [] 830 preserved_current: typing.Optional[subsequence.chords.Chord] = None 831 832 if self._harmonic_state is not None: 833 preserved_history = self._harmonic_state.history.copy() 834 preserved_current = self._harmonic_state.current_chord 835 836 self._harmonic_state = subsequence.harmonic_state.HarmonicState( 837 key_name = self.key, 838 graph_style = style, 839 include_dominant_7th = dominant_7th, 840 key_gravity_blend = gravity, 841 nir_strength = nir_strength, 842 minor_turnaround_weight = minor_turnaround_weight, 843 root_diversity = root_diversity 844 ) 845 846 if preserved_history: 847 self._harmonic_state.history = preserved_history 848 if preserved_current is not None and self._harmonic_state.graph.get_transitions(preserved_current): 849 self._harmonic_state.current_chord = preserved_current 850 851 self._harmony_cycle_beats = cycle_beats 852 self._harmony_reschedule_lookahead = reschedule_lookahead
Configure the harmonic logic and chord change intervals.
Subsequence uses a weighted transition graph to choose the next chord. You can influence these choices using 'gravity' (favoring the tonic) and 'NIR strength' (melodic inertia based on Narmour's model).
Arguments:
- style: The harmonic style to use. Built-in: "functional_major" (alias "diatonic_major"), "turnaround", "aeolian_minor", "phrygian_minor", "lydian_major", "dorian_minor", "chromatic_mediant", "suspended", "mixolydian", "whole_tone", "diminished". See README for full descriptions.
- cycle_beats: How many beats each chord lasts (default 4).
- dominant_7th: Whether to include V7 chords (default True).
- gravity: Key gravity (0.0 to 1.0). High values stay closer to the root chord.
- nir_strength: Melodic inertia (0.0 to 1.0). Influences chord movement expectations.
- minor_turnaround_weight: For "turnaround" style, influences major vs minor feel.
- root_diversity: Root-repetition damping (0.0 to 1.0). Each recent chord sharing a candidate's root reduces the weight to 40% at the default (0.4). Set to 1.0 to disable.
- reschedule_lookahead: How many beats in advance to calculate the next chord.
Example:
# A moody minor progression that changes every 8 beats comp.harmony(style="aeolian_minor", cycle_beats=8, gravity=0.4)
854 def freeze (self, bars: int) -> "Progression": 855 856 """Capture a chord progression from the live harmony engine. 857 858 Runs the harmony engine forward by *bars* chord changes, records each 859 chord, and returns it as a :class:`Progression` that can be bound to a 860 form section with :meth:`section_chords`. 861 862 The engine state **advances** — successive ``freeze()`` calls produce a 863 continuing compositional journey so section progressions feel like parts 864 of a whole rather than isolated islands. 865 866 Parameters: 867 bars: Number of chords to capture (one per harmony cycle). 868 869 Returns: 870 A :class:`Progression` with the captured chords and trailing 871 history for NIR continuity. 872 873 Raises: 874 ValueError: If :meth:`harmony` has not been called first. 875 876 Example:: 877 878 composition.harmony(style="functional_major", cycle_beats=4) 879 verse = composition.freeze(8) # 8 chords, engine advances 880 chorus = composition.freeze(4) # next 4 chords, continuing on 881 composition.section_chords("verse", verse) 882 composition.section_chords("chorus", chorus) 883 """ 884 885 hs = self._require_harmonic_state() 886 887 if bars < 1: 888 raise ValueError("bars must be at least 1") 889 collected: typing.List[subsequence.chords.Chord] = [hs.current_chord] 890 891 for _ in range(bars - 1): 892 hs.step() 893 collected.append(hs.current_chord) 894 895 # Advance past the last captured chord so the next freeze() call or 896 # live playback does not duplicate it. 897 hs.step() 898 899 return Progression( 900 chords = tuple(collected), 901 trailing_history = tuple(hs.history), 902 )
Capture a chord progression from the live harmony engine.
Runs the harmony engine forward by bars chord changes, records each
chord, and returns it as a Progression that can be bound to a
form section with section_chords().
The engine state advances — successive freeze() calls produce a
continuing compositional journey so section progressions feel like parts
of a whole rather than isolated islands.
Arguments:
- bars: Number of chords to capture (one per harmony cycle).
Returns:
A
Progressionwith the captured chords and trailing history for NIR continuity.
Raises:
- ValueError: If
harmony()has not been called first.
Example::
composition.harmony(style="functional_major", cycle_beats=4)
verse = composition.freeze(8) # 8 chords, engine advances
chorus = composition.freeze(4) # next 4 chords, continuing on
composition.section_chords("verse", verse)
composition.section_chords("chorus", chorus)
904 def section_chords (self, section_name: str, progression: "Progression") -> None: 905 906 """Bind a frozen :class:`Progression` to a named form section. 907 908 Every time *section_name* plays, the harmonic clock replays the 909 progression's chords in order instead of calling the live engine. 910 Sections without a bound progression continue generating live chords. 911 912 Parameters: 913 section_name: Name of the section as defined in :meth:`form`. 914 progression: The :class:`Progression` returned by :meth:`freeze`. 915 916 Raises: 917 ValueError: If the form has been configured and *section_name* is 918 not a known section name. 919 920 Example:: 921 922 composition.section_chords("verse", verse_progression) 923 composition.section_chords("chorus", chorus_progression) 924 # "bridge" is not bound — it generates live chords 925 """ 926 927 if ( 928 self._form_state is not None 929 and self._form_state._section_bars is not None 930 and section_name not in self._form_state._section_bars 931 ): 932 known = ", ".join(sorted(self._form_state._section_bars)) 933 raise ValueError( 934 f"Section '{section_name}' not found in form. " 935 f"Known sections: {known}" 936 ) 937 938 self._section_progressions[section_name] = progression
Bind a frozen Progression to a named form section.
Every time section_name plays, the harmonic clock replays the progression's chords in order instead of calling the live engine. Sections without a bound progression continue generating live chords.
Arguments:
- section_name: Name of the section as defined in
form(). - progression: The
Progressionreturned byfreeze().
Raises:
- ValueError: If the form has been configured and section_name is not a known section name.
Example::
composition.section_chords("verse", verse_progression)
composition.section_chords("chorus", chorus_progression)
# "bridge" is not bound — it generates live chords
940 def on_event (self, event_name: str, callback: typing.Callable[..., typing.Any]) -> None: 941 942 """ 943 Register a callback for a sequencer event (e.g., "bar", "start", "stop"). 944 """ 945 946 self._sequencer.on_event(event_name, callback)
Register a callback for a sequencer event (e.g., "bar", "start", "stop").
953 def hotkeys (self, enabled: bool = True) -> None: 954 955 """Enable or disable the global hotkey listener. 956 957 Must be called **before** :meth:`play` to take effect. When enabled, a 958 background thread reads single keystrokes from stdin without requiring 959 Enter. The ``?`` key is always reserved and lists all active bindings. 960 961 Hotkeys have zero impact on playback when disabled — the listener 962 thread is never started. 963 964 Args: 965 enabled: ``True`` (default) to enable hotkeys; ``False`` to disable. 966 967 Example:: 968 969 composition.hotkeys() 970 composition.hotkey("a", lambda: composition.form_jump("chorus")) 971 composition.play() 972 """ 973 974 self._hotkeys_enabled = enabled
Enable or disable the global hotkey listener.
Must be called before play() to take effect. When enabled, a
background thread reads single keystrokes from stdin without requiring
Enter. The ? key is always reserved and lists all active bindings.
Hotkeys have zero impact on playback when disabled — the listener thread is never started.
Arguments:
- enabled:
True(default) to enable hotkeys;Falseto disable.
Example::
composition.hotkeys()
composition.hotkey("a", lambda: composition.form_jump("chorus"))
composition.play()
977 def hotkey ( 978 self, 979 key: str, 980 action: typing.Callable[[], None], 981 quantize: int = 0, 982 label: typing.Optional[str] = None, 983 ) -> None: 984 985 """Register a single-key shortcut that fires during playback. 986 987 The listener must be enabled first with :meth:`hotkeys`. 988 989 Most actions — form jumps, ``composition.data`` writes, and 990 :meth:`tweak` calls — should use ``quantize=0`` (the default). Their 991 musical effect is naturally delayed to the next pattern rebuild cycle, 992 which provides automatic musical quantization without extra configuration. 993 994 Use ``quantize=N`` for actions where you want an explicit bar-boundary 995 guarantee, such as :meth:`mute` / :meth:`unmute`. 996 997 The ``?`` key is reserved and cannot be overridden. 998 999 Args: 1000 key: A single character trigger (e.g. ``"a"``, ``"1"``, ``" "``). 1001 action: Zero-argument callable to execute. 1002 quantize: ``0`` = execute immediately (default). ``N`` = execute 1003 on the next global bar number divisible by *N*. 1004 label: Display name for the ``?`` help listing. Auto-derived from 1005 the function name or lambda body if omitted. 1006 1007 Raises: 1008 ValueError: If ``key`` is the reserved ``?`` character, or if 1009 ``key`` is not exactly one character. 1010 1011 Example:: 1012 1013 composition.hotkeys() 1014 1015 # Immediate — musical effect happens at next pattern rebuild 1016 composition.hotkey("a", lambda: composition.form_jump("chorus")) 1017 composition.hotkey("1", lambda: composition.data.update({"mode": "chill"})) 1018 1019 # Explicit 4-bar phrase boundary 1020 composition.hotkey("s", lambda: composition.mute("drums"), quantize=4) 1021 1022 # Named function — label is derived automatically 1023 def drop_to_breakdown (): 1024 composition.form_jump("breakdown") 1025 composition.mute("lead") 1026 1027 composition.hotkey("d", drop_to_breakdown) 1028 1029 composition.play() 1030 """ 1031 1032 if len(key) != 1: 1033 raise ValueError(f"hotkey key must be a single character, got {key!r}") 1034 1035 if key == _HOTKEY_RESERVED: 1036 raise ValueError(f"'{_HOTKEY_RESERVED}' is reserved for listing active hotkeys.") 1037 1038 derived = label if label is not None else _derive_label(action) 1039 1040 self._hotkey_bindings[key] = HotkeyBinding( 1041 key = key, 1042 action = action, 1043 quantize = quantize, 1044 label = derived, 1045 )
Register a single-key shortcut that fires during playback.
The listener must be enabled first with hotkeys().
Most actions — form jumps, composition.data writes, and
tweak() calls — should use quantize=0 (the default). Their
musical effect is naturally delayed to the next pattern rebuild cycle,
which provides automatic musical quantization without extra configuration.
Use quantize=N for actions where you want an explicit bar-boundary
guarantee, such as mute() / unmute().
The ? key is reserved and cannot be overridden.
Arguments:
- key: A single character trigger (e.g.
"a","1"," "). - action: Zero-argument callable to execute.
- quantize:
0= execute immediately (default).N= execute on the next global bar number divisible by N. - label: Display name for the
?help listing. Auto-derived from the function name or lambda body if omitted.
Raises:
Example::
composition.hotkeys()
# Immediate — musical effect happens at next pattern rebuild
composition.hotkey("a", lambda: composition.form_jump("chorus"))
composition.hotkey("1", lambda: composition.data.update({"mode": "chill"}))
# Explicit 4-bar phrase boundary
composition.hotkey("s", lambda: composition.mute("drums"), quantize=4)
# Named function — label is derived automatically
def drop_to_breakdown ():
composition.form_jump("breakdown")
composition.mute("lead")
composition.hotkey("d", drop_to_breakdown)
composition.play()
1048 def form_jump (self, section_name: str) -> None: 1049 1050 """Jump the form to a named section immediately. 1051 1052 Delegates to :meth:`subsequence.form_state.FormState.jump_to`. Only works when the 1053 composition uses graph-mode form (a dict passed to :meth:`form`). 1054 1055 The musical effect is heard at the *next pattern rebuild cycle* — already- 1056 queued MIDI notes are unaffected. This natural delay means ``form_jump`` 1057 is effective without needing explicit quantization. 1058 1059 Args: 1060 section_name: The section to jump to. 1061 1062 Raises: 1063 ValueError: If no form is configured, or the form is not in graph 1064 mode, or *section_name* is unknown. 1065 1066 Example:: 1067 1068 composition.hotkey("c", lambda: composition.form_jump("chorus")) 1069 """ 1070 1071 if self._form_state is None: 1072 raise ValueError("form_jump() requires a form to be configured via composition.form().") 1073 1074 self._form_state.jump_to(section_name)
Jump the form to a named section immediately.
Delegates to subsequence.form_state.FormState.jump_to(). Only works when the
composition uses graph-mode form (a dict passed to form()).
The musical effect is heard at the next pattern rebuild cycle — already-
queued MIDI notes are unaffected. This natural delay means form_jump
is effective without needing explicit quantization.
Arguments:
- section_name: The section to jump to.
Raises:
- ValueError: If no form is configured, or the form is not in graph mode, or section_name is unknown.
Example::
composition.hotkey("c", lambda: composition.form_jump("chorus"))
1077 def form_next (self, section_name: str) -> None: 1078 1079 """Queue the next section — takes effect when the current section ends. 1080 1081 Unlike :meth:`form_jump`, this does not interrupt the current section. 1082 The queued section replaces the automatically pre-decided next section 1083 and takes effect at the natural section boundary. The performer can 1084 change their mind by calling ``form_next`` again before the boundary. 1085 1086 Delegates to :meth:`subsequence.form_state.FormState.queue_next`. Only works when the 1087 composition uses graph-mode form (a dict passed to :meth:`form`). 1088 1089 Args: 1090 section_name: The section to queue. 1091 1092 Raises: 1093 ValueError: If no form is configured, or the form is not in graph 1094 mode, or *section_name* is unknown. 1095 1096 Example:: 1097 1098 composition.hotkey("c", lambda: composition.form_next("chorus")) 1099 """ 1100 1101 if self._form_state is None: 1102 raise ValueError("form_next() requires a form to be configured via composition.form().") 1103 1104 self._form_state.queue_next(section_name)
Queue the next section — takes effect when the current section ends.
Unlike form_jump(), this does not interrupt the current section.
The queued section replaces the automatically pre-decided next section
and takes effect at the natural section boundary. The performer can
change their mind by calling form_next again before the boundary.
Delegates to subsequence.form_state.FormState.queue_next(). Only works when the
composition uses graph-mode form (a dict passed to form()).
Arguments:
- section_name: The section to queue.
Raises:
- ValueError: If no form is configured, or the form is not in graph mode, or section_name is unknown.
Example::
composition.hotkey("c", lambda: composition.form_next("chorus"))
1187 def seed (self, value: int) -> None: 1188 1189 """ 1190 Set a random seed for deterministic, repeatable playback. 1191 1192 If a seed is set, Subsequence will produce the exact same sequence 1193 every time you run the script. This is vital for finishing tracks or 1194 reproducing a specific 'performance'. 1195 1196 Parameters: 1197 value: An integer seed. 1198 1199 Example: 1200 ```python 1201 # Fix the randomness 1202 comp.seed(42) 1203 ``` 1204 """ 1205 1206 self._seed = value
Set a random seed for deterministic, repeatable playback.
If a seed is set, Subsequence will produce the exact same sequence every time you run the script. This is vital for finishing tracks or reproducing a specific 'performance'.
Arguments:
- value: An integer seed.
Example:
# Fix the randomness comp.seed(42)
1208 def tuning ( 1209 self, 1210 source: typing.Optional[typing.Union[str, "os.PathLike"]] = None, 1211 *, 1212 cents: typing.Optional[typing.List[float]] = None, 1213 ratios: typing.Optional[typing.List[float]] = None, 1214 equal: typing.Optional[int] = None, 1215 bend_range: float = 2.0, 1216 channels: typing.Optional[typing.List[int]] = None, 1217 reference_note: int = 60, 1218 exclude_drums: bool = True, 1219 ) -> None: 1220 1221 """Set a global microtonal tuning for the composition. 1222 1223 The tuning is applied automatically after each pattern rebuild (before 1224 the pattern is scheduled). Drum patterns (those registered with a 1225 ``drum_note_map``) are excluded by default. 1226 1227 Supply exactly one of the source parameters: 1228 1229 - ``source``: path to a Scala ``.scl`` file. 1230 - ``cents``: list of cent offsets for degrees 1..N (degree 0 = 0.0 is implicit). 1231 - ``ratios``: list of frequency ratios (e.g., ``[9/8, 5/4, 4/3, 3/2, 2]``). 1232 - ``equal``: integer for N-tone equal temperament (e.g., ``equal=19``). 1233 1234 For polyphonic parts, supply a ``channels`` pool. Notes are spread 1235 across those MIDI channels so each can carry an independent pitch bend. 1236 The synth must be configured to match ``bend_range`` (its pitch-bend range 1237 setting in semitones). 1238 1239 Parameters: 1240 source: Path to a ``.scl`` file. 1241 cents: Cent offsets for scale degrees 1..N. 1242 ratios: Frequency ratios for scale degrees 1..N. 1243 equal: Number of equal divisions of the period. 1244 bend_range: Synth pitch-bend range in semitones (default ±2). 1245 channels: Channel pool for polyphonic rotation. 1246 reference_note: MIDI note mapped to scale degree 0 (default 60 = C4). 1247 exclude_drums: When True (default), skip patterns that have a 1248 ``drum_note_map`` (they use fixed GM pitches, not tuned ones). 1249 1250 Example: 1251 ```python 1252 # Quarter-comma meantone from a Scala file 1253 comp.tuning("meanquar.scl") 1254 1255 # Just intonation from ratios 1256 comp.tuning(ratios=[9/8, 5/4, 4/3, 3/2, 5/3, 15/8, 2]) 1257 1258 # 19-TET, monophonic 1259 comp.tuning(equal=19, bend_range=2.0) 1260 1261 # 31-TET with channel rotation for polyphony (channels 1-6) 1262 comp.tuning("31tet.scl", channels=[0, 1, 2, 3, 4, 5]) 1263 ``` 1264 """ 1265 import subsequence.tuning as _tuning_mod 1266 1267 given = sum(x is not None for x in [source, cents, ratios, equal]) 1268 if given == 0: 1269 raise ValueError("composition.tuning() requires one of: source, cents, ratios, or equal") 1270 if given > 1: 1271 raise ValueError("composition.tuning() accepts only one source parameter") 1272 1273 if source is not None: 1274 t = _tuning_mod.Tuning.from_scl(source) 1275 elif cents is not None: 1276 t = _tuning_mod.Tuning.from_cents(cents) 1277 elif ratios is not None: 1278 t = _tuning_mod.Tuning.from_ratios(ratios) 1279 else: 1280 t = _tuning_mod.Tuning.equal(equal) # type: ignore[arg-type] 1281 1282 self._tuning = t 1283 self._tuning_bend_range = bend_range 1284 self._tuning_channels = channels 1285 self._tuning_reference_note = reference_note 1286 self._tuning_exclude_drums = exclude_drums
Set a global microtonal tuning for the composition.
The tuning is applied automatically after each pattern rebuild (before
the pattern is scheduled). Drum patterns (those registered with a
drum_note_map) are excluded by default.
Supply exactly one of the source parameters:
source: path to a Scala.sclfile.cents: list of cent offsets for degrees 1..N (degree 0 = 0.0 is implicit).ratios: list of frequency ratios (e.g.,[9/8, 5/4, 4/3, 3/2, 2]).equal: integer for N-tone equal temperament (e.g.,equal=19).
For polyphonic parts, supply a channels pool. Notes are spread
across those MIDI channels so each can carry an independent pitch bend.
The synth must be configured to match bend_range (its pitch-bend range
setting in semitones).
Arguments:
- source: Path to a
.sclfile. - cents: Cent offsets for scale degrees 1..N.
- ratios: Frequency ratios for scale degrees 1..N.
- equal: Number of equal divisions of the period.
- bend_range: Synth pitch-bend range in semitones (default ±2).
- channels: Channel pool for polyphonic rotation.
- reference_note: MIDI note mapped to scale degree 0 (default 60 = C4).
- exclude_drums: When True (default), skip patterns that have a
drum_note_map(they use fixed GM pitches, not tuned ones).
Example:
# Quarter-comma meantone from a Scala file comp.tuning("meanquar.scl") # Just intonation from ratios comp.tuning(ratios=[9/8, 5/4, 4/3, 3/2, 5/3, 15/8, 2]) # 19-TET, monophonic comp.tuning(equal=19, bend_range=2.0) # 31-TET with channel rotation for polyphony (channels 1-6) comp.tuning("31tet.scl", channels=[0, 1, 2, 3, 4, 5])
1288 def display (self, enabled: bool = True, grid: bool = False, grid_scale: float = 1.0) -> None: 1289 1290 """ 1291 Enable or disable the live terminal dashboard. 1292 1293 When enabled, Subsequence uses a safe logging handler that allows a 1294 persistent status line (BPM, Key, Bar, Section, Chord) to stay at 1295 the bottom of the terminal while logs scroll above it. 1296 1297 Parameters: 1298 enabled: Whether to show the display (default True). 1299 grid: When True, render an ASCII grid visualisation of all 1300 running patterns above the status line. The grid updates 1301 once per bar, showing which steps have notes and at what 1302 velocity. 1303 grid_scale: Horizontal zoom factor for the grid (default 1304 ``1.0``). Higher values add visual columns between 1305 grid steps, revealing micro-timing from swing and groove. 1306 Snapped to the nearest integer internally for uniform 1307 marker spacing. 1308 """ 1309 1310 if enabled: 1311 self._display = subsequence.display.Display(self, grid=grid, grid_scale=grid_scale) 1312 else: 1313 self._display = None
Enable or disable the live terminal dashboard.
When enabled, Subsequence uses a safe logging handler that allows a persistent status line (BPM, Key, Bar, Section, Chord) to stay at the bottom of the terminal while logs scroll above it.
Arguments:
- enabled: Whether to show the display (default True).
- grid: When True, render an ASCII grid visualisation of all running patterns above the status line. The grid updates once per bar, showing which steps have notes and at what velocity.
- grid_scale: Horizontal zoom factor for the grid (default
1.0). Higher values add visual columns between grid steps, revealing micro-timing from swing and groove. Snapped to the nearest integer internally for uniform marker spacing.
1315 def web_ui (self) -> None: 1316 1317 """ 1318 Enable the realtime Web UI Dashboard. 1319 1320 When enabled, Subsequence instantiates a WebSocket server that broadcasts 1321 the current state, signals, and active patterns (with high-res timing and note data) 1322 to any connected browser clients. 1323 """ 1324 1325 self._web_ui_enabled = True
Enable the realtime Web UI Dashboard.
When enabled, Subsequence instantiates a WebSocket server that broadcasts the current state, signals, and active patterns (with high-res timing and note data) to any connected browser clients.
1327 def midi_input (self, device: str, clock_follow: bool = False, name: typing.Optional[str] = None) -> None: 1328 1329 """ 1330 Configure a MIDI input device for external sync and MIDI messages. 1331 1332 May be called multiple times to register additional input devices. 1333 The first call sets the primary input (device 0). Subsequent calls 1334 add additional input devices (device 1, 2, …). Only one device may 1335 have ``clock_follow=True``. 1336 1337 Parameters: 1338 device: The name of the MIDI input port. 1339 clock_follow: If True, Subsequence will slave its clock to incoming 1340 MIDI Ticks. It will also follow MIDI Start/Stop/Continue 1341 commands. Only one device can have this enabled at a time. 1342 name: Optional alias for use with ``cc_map(input_device=…)`` and 1343 ``cc_forward(input_device=…)``. When omitted, the raw device 1344 name is used. 1345 1346 Example: 1347 ```python 1348 # Single controller (unchanged usage) 1349 comp.midi_input("Scarlett 2i4", clock_follow=True) 1350 1351 # Multiple controllers 1352 comp.midi_input("Arturia KeyStep", name="keys") 1353 comp.midi_input("Faderfox EC4", name="faders") 1354 ``` 1355 """ 1356 1357 if clock_follow: 1358 if self.is_clock_following: 1359 raise ValueError("Only one input device can be configured to follow external clock (clock_follow=True)") 1360 1361 if self._input_device is None: 1362 # First call: set primary input device (device 0) 1363 self._input_device = device 1364 self._input_device_alias = name 1365 self._clock_follow = clock_follow 1366 else: 1367 # Subsequent calls: register additional input devices 1368 self._additional_inputs.append((device, name, clock_follow))
Configure a MIDI input device for external sync and MIDI messages.
May be called multiple times to register additional input devices.
The first call sets the primary input (device 0). Subsequent calls
add additional input devices (device 1, 2, …). Only one device may
have clock_follow=True.
Arguments:
- device: The name of the MIDI input port.
- clock_follow: If True, Subsequence will slave its clock to incoming MIDI Ticks. It will also follow MIDI Start/Stop/Continue commands. Only one device can have this enabled at a time.
- name: Optional alias for use with
cc_map(input_device=…)andcc_forward(input_device=…). When omitted, the raw device name is used.
Example:
# Single controller (unchanged usage) comp.midi_input("Scarlett 2i4", clock_follow=True) # Multiple controllers comp.midi_input("Arturia KeyStep", name="keys") comp.midi_input("Faderfox EC4", name="faders")
1370 def midi_output (self, device: str, name: typing.Optional[str] = None) -> int: 1371 1372 """ 1373 Register an additional MIDI output device. 1374 1375 The first output device is always the one passed to 1376 ``Composition(output_device=…)`` — that is device 0. 1377 Each call to ``midi_output()`` adds the next device (1, 2, …). 1378 1379 Parameters: 1380 device: The name of the MIDI output port. 1381 name: Optional alias for use with ``pattern(device=…)``, 1382 ``cc_forward(output_device=…)``, etc. When omitted, the raw 1383 device name is used. 1384 1385 Returns: 1386 The integer device index assigned (1, 2, 3, …). 1387 1388 Example: 1389 ```python 1390 comp = subsequence.Composition(bpm=120, output_device="MOTU Express") 1391 1392 # Returns 1 — use as device=1 or device="integra" 1393 comp.midi_output("Roland Integra", name="integra") 1394 1395 @comp.pattern(channel=1, beats=4, device="integra") 1396 def strings (p): 1397 p.note(60, beat=0) 1398 ``` 1399 """ 1400 1401 idx = 1 + len(self._additional_outputs) # device 0 is always the primary 1402 self._additional_outputs.append((device, name)) 1403 return idx
Register an additional MIDI output device.
The first output device is always the one passed to
Composition(output_device=…) — that is device 0.
Each call to midi_output() adds the next device (1, 2, …).
Arguments:
- device: The name of the MIDI output port.
- name: Optional alias for use with
pattern(device=…),cc_forward(output_device=…), etc. When omitted, the raw device name is used.
Returns:
The integer device index assigned (1, 2, 3, …).
Example:
comp = subsequence.Composition(bpm=120, output_device="MOTU Express") # Returns 1 — use as device=1 or device="integra" comp.midi_output("Roland Integra", name="integra") @comp.pattern(channel=1, beats=4, device="integra") def strings (p): p.note(60, beat=0)
1405 def clock_output (self, enabled: bool = True) -> None: 1406 1407 """ 1408 Send MIDI timing clock to connected hardware. 1409 1410 When enabled, Subsequence acts as a MIDI clock master and sends 1411 standard clock messages on the output port: a Start message (0xFA) 1412 when playback begins, a Clock tick (0xF8) on every pulse (24 PPQN), 1413 and a Stop message (0xFC) when playback ends. 1414 1415 This allows hardware synthesizers, drum machines, and effect units to 1416 slave their tempo to Subsequence automatically. 1417 1418 **Note:** Clock output is automatically disabled when ``midi_input()`` 1419 is called with ``clock_follow=True``, to prevent a clock feedback loop. 1420 1421 Parameters: 1422 enabled: Whether to send MIDI clock (default True). 1423 1424 Example: 1425 ```python 1426 comp = subsequence.Composition(bpm=120, output_device="...") 1427 comp.clock_output() # hardware will follow Subsequence tempo 1428 ``` 1429 """ 1430 1431 self._clock_output = enabled
Send MIDI timing clock to connected hardware.
When enabled, Subsequence acts as a MIDI clock master and sends standard clock messages on the output port: a Start message (0xFA) when playback begins, a Clock tick (0xF8) on every pulse (24 PPQN), and a Stop message (0xFC) when playback ends.
This allows hardware synthesizers, drum machines, and effect units to slave their tempo to Subsequence automatically.
Note: Clock output is automatically disabled when midi_input()
is called with clock_follow=True, to prevent a clock feedback loop.
Arguments:
- enabled: Whether to send MIDI clock (default True).
Example:
comp = subsequence.Composition(bpm=120, output_device="...") comp.clock_output() # hardware will follow Subsequence tempo
1434 def link (self, quantum: float = 4.0) -> "Composition": 1435 1436 """ 1437 Enable Ableton Link tempo and phase synchronisation. 1438 1439 When enabled, Subsequence joins the local Link session and slaves its 1440 clock to the shared network tempo and beat phase. All other Link-enabled 1441 apps on the same LAN — Ableton Live, iOS synths, other Subsequence 1442 instances — will automatically stay in time. 1443 1444 Playback starts on the next bar boundary aligned to the Link quantum, 1445 so downbeats stay in sync across all participants. 1446 1447 Requires the ``link`` optional extra:: 1448 1449 pip install subsequence[link] 1450 1451 Parameters: 1452 quantum: Beat cycle length. ``4.0`` (default) = one bar in 4/4 time. 1453 Change this if your composition uses a different meter. 1454 1455 Example:: 1456 1457 comp = subsequence.Composition(bpm=120, key="C") 1458 comp.link() # join the Link session 1459 comp.play() 1460 1461 # On another machine / instance: 1462 comp2 = subsequence.Composition(bpm=120) 1463 comp2.link() # tempo and phase will lock to comp 1464 comp2.play() 1465 1466 Note: 1467 ``set_bpm()`` proposes the new tempo to the Link network when Link 1468 is active. The network-authoritative tempo is applied on the next 1469 pulse, so there may be a brief lag before the change is visible. 1470 """ 1471 1472 # Eagerly check that aalink is installed — fail early with a clear message. 1473 subsequence.link_clock._require_aalink() 1474 1475 self._link_quantum = quantum 1476 return self
Enable Ableton Link tempo and phase synchronisation.
When enabled, Subsequence joins the local Link session and slaves its clock to the shared network tempo and beat phase. All other Link-enabled apps on the same LAN — Ableton Live, iOS synths, other Subsequence instances — will automatically stay in time.
Playback starts on the next bar boundary aligned to the Link quantum, so downbeats stay in sync across all participants.
Requires the link optional extra::
pip install subsequence[link]
Arguments:
- quantum: Beat cycle length.
4.0(default) = one bar in 4/4 time. Change this if your composition uses a different meter.
Example::
comp = subsequence.Composition(bpm=120, key="C")
comp.link() # join the Link session
comp.play()
# On another machine / instance:
comp2 = subsequence.Composition(bpm=120)
comp2.link() # tempo and phase will lock to comp
comp2.play()
Note:
set_bpm()proposes the new tempo to the Link network when Link is active. The network-authoritative tempo is applied on the next pulse, so there may be a brief lag before the change is visible.
1479 def cc_map ( 1480 self, 1481 cc: int, 1482 key: str, 1483 channel: typing.Optional[int] = None, 1484 min_val: float = 0.0, 1485 max_val: float = 1.0, 1486 input_device: subsequence.midi_utils.DeviceId = None, 1487 ) -> None: 1488 1489 """ 1490 Map an incoming MIDI CC to a ``composition.data`` key. 1491 1492 When the composition receives a CC message on the configured MIDI 1493 input port, the value is scaled from the CC range (0–127) to 1494 *[min_val, max_val]* and stored in ``composition.data[key]``. 1495 1496 This lets hardware knobs, faders, and expression pedals control live 1497 parameters without writing any callback code. 1498 1499 **Requires** ``midi_input()`` to be called first to open an input port. 1500 1501 Parameters: 1502 cc: MIDI Control Change number (0–127). 1503 key: The ``composition.data`` key to write. 1504 channel: If given, only respond to CC messages on this channel. 1505 Uses the same numbering convention as ``pattern()`` (0-15 1506 by default, or 1-16 with ``zero_indexed_channels=False``). 1507 ``None`` matches any channel (default). 1508 min_val: Scaled minimum — written when CC value is 0 (default 0.0). 1509 max_val: Scaled maximum — written when CC value is 127 (default 1.0). 1510 input_device: Only respond to CC messages from this input device 1511 (index or name). ``None`` responds to any input device (default). 1512 1513 Example: 1514 ```python 1515 comp.midi_input("Arturia KeyStep") 1516 comp.cc_map(74, "filter_cutoff") # knob → 0.0–1.0 1517 comp.cc_map(7, "volume", min_val=0, max_val=127) # volume fader 1518 1519 # Multi-device: only listen to CC 74 from the "faders" controller 1520 comp.cc_map(74, "filter", input_device="faders") 1521 ``` 1522 """ 1523 1524 resolved_channel = self._resolve_channel(channel) if channel is not None else None 1525 1526 self._cc_mappings.append({ 1527 'cc': cc, 1528 'key': key, 1529 'channel': resolved_channel, 1530 'min_val': min_val, 1531 'max_val': max_val, 1532 'input_device': input_device, # resolved to int index in _run() 1533 })
Map an incoming MIDI CC to a composition.data key.
When the composition receives a CC message on the configured MIDI
input port, the value is scaled from the CC range (0–127) to
[min_val, max_val] and stored in composition.data[key].
This lets hardware knobs, faders, and expression pedals control live parameters without writing any callback code.
Requires midi_input() to be called first to open an input port.
Arguments:
- cc: MIDI Control Change number (0–127).
- key: The
composition.datakey to write. - channel: If given, only respond to CC messages on this channel.
Uses the same numbering convention as
pattern()(0-15 by default, or 1-16 withzero_indexed_channels=False).Nonematches any channel (default). - min_val: Scaled minimum — written when CC value is 0 (default 0.0).
- max_val: Scaled maximum — written when CC value is 127 (default 1.0).
- input_device: Only respond to CC messages from this input device
(index or name).
Noneresponds to any input device (default).
Example:
comp.midi_input("Arturia KeyStep") comp.cc_map(74, "filter_cutoff") # knob → 0.0–1.0 comp.cc_map(7, "volume", min_val=0, max_val=127) # volume fader # Multi-device: only listen to CC 74 from the "faders" controller comp.cc_map(74, "filter", input_device="faders")
1595 def cc_forward ( 1596 self, 1597 cc: int, 1598 output: typing.Union[str, typing.Callable], 1599 *, 1600 channel: typing.Optional[int] = None, 1601 output_channel: typing.Optional[int] = None, 1602 mode: str = "instant", 1603 input_device: subsequence.midi_utils.DeviceId = None, 1604 output_device: subsequence.midi_utils.DeviceId = None, 1605 ) -> None: 1606 1607 """ 1608 Forward an incoming MIDI CC to the MIDI output in real-time. 1609 1610 Unlike ``cc_map()`` which writes incoming CC values to ``composition.data`` 1611 for use at pattern rebuild time, ``cc_forward()`` routes the signal 1612 directly to the MIDI output — bypassing the pattern cycle entirely. 1613 1614 Both ``cc_map()`` and ``cc_forward()`` may be registered for the same CC 1615 number; they operate independently. 1616 1617 Parameters: 1618 cc: Incoming CC number to listen for (0–127). 1619 output: What to send. Either a **preset string**: 1620 1621 - ``"cc"`` — identity forward, same CC number and value. 1622 - ``"cc:N"`` — forward as CC number N (e.g. ``"cc:74"``). 1623 - ``"pitchwheel"`` — scale 0–127 to -8192..8191 and send as pitch bend. 1624 1625 Or a **callable** with signature 1626 ``(value: int, channel: int) -> Optional[mido.Message]``. 1627 Return a fully formed ``mido.Message`` to send, or ``None`` to suppress. 1628 ``channel`` is 0-indexed (the incoming channel). 1629 channel: If given, only respond to CC messages on this channel. 1630 Uses the same numbering convention as ``cc_map()``. 1631 ``None`` matches any channel (default). 1632 output_channel: Override the output channel. ``None`` uses the 1633 incoming channel. Uses the same numbering convention as ``pattern()``. 1634 mode: Dispatch mode: 1635 1636 - ``"instant"`` *(default)* — send immediately on the MIDI input 1637 callback thread. Lowest latency (~1–5 ms). Instant forwards are 1638 **not** recorded when recording is enabled. 1639 - ``"queued"`` — inject into the sequencer event queue and send at 1640 the next pulse boundary (~0–20 ms at 120 BPM). Queued forwards 1641 **are** recorded when recording is enabled. 1642 1643 Example: 1644 ```python 1645 comp.midi_input("Arturia KeyStep") 1646 1647 # CC 1 → CC 1 (identity, instant) 1648 comp.cc_forward(1, "cc") 1649 1650 # CC 1 → pitch bend on channel 1, queued (recordable) 1651 comp.cc_forward(1, "pitchwheel", output_channel=1, mode="queued") 1652 1653 # CC 1 → CC 74, custom channel 1654 comp.cc_forward(1, "cc:74", output_channel=2) 1655 1656 # Custom transform — remap CC range 0–127 to CC 74 range 40–100 1657 import subsequence.midi as midi 1658 comp.cc_forward(1, lambda v, ch: midi.cc(74, int(v / 127 * 60) + 40, channel=ch)) 1659 1660 # Forward AND map to data simultaneously — both active on the same CC 1661 comp.cc_map(1, "mod_wheel") 1662 comp.cc_forward(1, "cc:74") 1663 ``` 1664 """ 1665 1666 if not 0 <= cc <= 127: 1667 raise ValueError(f"cc_forward(): cc {cc} out of range 0–127") 1668 1669 if mode not in ('instant', 'queued'): 1670 raise ValueError(f"cc_forward(): mode must be 'instant' or 'queued', got '{mode}'") 1671 1672 resolved_in_channel = self._resolve_channel(channel) if channel is not None else None 1673 resolved_out_channel = self._resolve_channel(output_channel) if output_channel is not None else None 1674 1675 transform = self._make_cc_forward_transform(output, cc, resolved_out_channel) 1676 1677 self._cc_forwards.append({ 1678 'cc': cc, 1679 'channel': resolved_in_channel, 1680 'output_channel': resolved_out_channel, 1681 'mode': mode, 1682 'transform': transform, 1683 'input_device': input_device, # resolved to int index in _run() 1684 'output_device': output_device, # resolved to int index in _run() 1685 })
Forward an incoming MIDI CC to the MIDI output in real-time.
Unlike cc_map() which writes incoming CC values to composition.data
for use at pattern rebuild time, cc_forward() routes the signal
directly to the MIDI output — bypassing the pattern cycle entirely.
Both cc_map() and cc_forward() may be registered for the same CC
number; they operate independently.
Arguments:
- cc: Incoming CC number to listen for (0–127).
output: What to send. Either a preset string:
"cc"— identity forward, same CC number and value."cc:N"— forward as CC number N (e.g."cc:74")."pitchwheel"— scale 0–127 to -8192..8191 and send as pitch bend.
Or a callable with signature
(value: int, channel: int) -> Optional[mido.Message]. Return a fully formedmido.Messageto send, orNoneto suppress.channelis 0-indexed (the incoming channel).- channel: If given, only respond to CC messages on this channel.
Uses the same numbering convention as
cc_map().Nonematches any channel (default). - output_channel: Override the output channel.
Noneuses the incoming channel. Uses the same numbering convention aspattern(). mode: Dispatch mode:
"instant"(default) — send immediately on the MIDI input callback thread. Lowest latency (~1–5 ms). Instant forwards are not recorded when recording is enabled."queued"— inject into the sequencer event queue and send at the next pulse boundary (~0–20 ms at 120 BPM). Queued forwards are recorded when recording is enabled.
Example:
comp.midi_input("Arturia KeyStep") # CC 1 → CC 1 (identity, instant) comp.cc_forward(1, "cc") # CC 1 → pitch bend on channel 1, queued (recordable) comp.cc_forward(1, "pitchwheel", output_channel=1, mode="queued") # CC 1 → CC 74, custom channel comp.cc_forward(1, "cc:74", output_channel=2) # Custom transform — remap CC range 0–127 to CC 74 range 40–100 import subsequence.midi as midi comp.cc_forward(1, lambda v, ch: midi.cc(74, int(v / 127 * 60) + 40, channel=ch)) # Forward AND map to data simultaneously — both active on the same CC comp.cc_map(1, "mod_wheel") comp.cc_forward(1, "cc:74")
1688 def live (self, port: int = 5555) -> None: 1689 1690 """ 1691 Enable the live coding eval server. 1692 1693 This allows you to connect to a running composition using the 1694 `subsequence.live_client` REPL and hot-swap pattern code or 1695 modify variables in real-time. 1696 1697 Parameters: 1698 port: The TCP port to listen on (default 5555). 1699 """ 1700 1701 self._live_server = subsequence.live_server.LiveServer(self, port=port) 1702 self._is_live = True
Enable the live coding eval server.
This allows you to connect to a running composition using the
subsequence.live_client REPL and hot-swap pattern code or
modify variables in real-time.
Arguments:
- port: The TCP port to listen on (default 5555).
1704 def osc (self, receive_port: int = 9000, send_port: int = 9001, send_host: str = "127.0.0.1") -> None: 1705 1706 """ 1707 Enable bi-directional Open Sound Control (OSC). 1708 1709 Subsequence will listen for commands (like `/bpm` or `/mute`) and 1710 broadcast its internal state (like `/chord` or `/bar`) over UDP. 1711 1712 Parameters: 1713 receive_port: Port to listen for incoming OSC messages (default 9000). 1714 send_port: Port to send state updates to (default 9001). 1715 send_host: The IP address to send updates to (default "127.0.0.1"). 1716 """ 1717 1718 self._osc_server = subsequence.osc.OscServer( 1719 self, 1720 receive_port = receive_port, 1721 send_port = send_port, 1722 send_host = send_host 1723 )
Enable bi-directional Open Sound Control (OSC).
Subsequence will listen for commands (like /bpm or /mute) and
broadcast its internal state (like /chord or /bar) over UDP.
Arguments:
- receive_port: Port to listen for incoming OSC messages (default 9000).
- send_port: Port to send state updates to (default 9001).
- send_host: The IP address to send updates to (default "127.0.0.1").
1725 def osc_map (self, address: str, handler: typing.Callable) -> None: 1726 1727 """ 1728 Register a custom OSC handler. 1729 1730 Must be called after :meth:`osc` has been configured. 1731 1732 Parameters: 1733 address: OSC address pattern to match (e.g. ``"/my/param"``). 1734 handler: Callable invoked with ``(address, *args)`` when a 1735 matching message arrives. 1736 1737 Example:: 1738 1739 composition.osc("/control") 1740 1741 def on_intensity (address, value): 1742 composition.data["intensity"] = float(value) 1743 1744 composition.osc_map("/intensity", on_intensity) 1745 """ 1746 1747 if self._osc_server is None: 1748 raise RuntimeError("Call composition.osc() before composition.osc_map()") 1749 1750 self._osc_server.map(address, handler)
Register a custom OSC handler.
Must be called after osc() has been configured.
Arguments:
- address: OSC address pattern to match (e.g.
"/my/param"). - handler: Callable invoked with
(address, *args)when a matching message arrives.
Example::
composition.osc("/control")
def on_intensity (address, value):
composition.data["intensity"] = float(value)
composition.osc_map("/intensity", on_intensity)
1752 def set_bpm (self, bpm: float) -> None: 1753 1754 """ 1755 Instantly change the tempo. 1756 1757 Parameters: 1758 bpm: The new tempo in beats per minute. 1759 1760 When Ableton Link is active, this proposes the new tempo to the Link 1761 network instead of applying it locally. The network-authoritative tempo 1762 is picked up on the next pulse. 1763 """ 1764 1765 self._sequencer.set_bpm(bpm) 1766 1767 if not self.is_clock_following and self._link_quantum is None: 1768 self.bpm = bpm
Instantly change the tempo.
Arguments:
- bpm: The new tempo in beats per minute.
When Ableton Link is active, this proposes the new tempo to the Link network instead of applying it locally. The network-authoritative tempo is picked up on the next pulse.
1770 def target_bpm (self, bpm: float, bars: int, shape: str = "linear") -> None: 1771 1772 """ 1773 Smoothly ramp the tempo to a target value over a number of bars. 1774 1775 Parameters: 1776 bpm: Target tempo in beats per minute. 1777 bars: Duration of the transition in bars. 1778 shape: Easing curve name. Defaults to ``"linear"``. 1779 ``"ease_in_out"`` or ``"s_curve"`` are recommended for natural- 1780 sounding tempo changes. See :mod:`subsequence.easing` for all 1781 available shapes. 1782 1783 Example: 1784 ```python 1785 # Accelerate to 140 BPM over the next 8 bars with a smooth S-curve 1786 comp.target_bpm(140, bars=8, shape="ease_in_out") 1787 ``` 1788 """ 1789 1790 self._sequencer.set_target_bpm(bpm, bars, shape)
Smoothly ramp the tempo to a target value over a number of bars.
Arguments:
- bpm: Target tempo in beats per minute.
- bars: Duration of the transition in bars.
- shape: Easing curve name. Defaults to
"linear"."ease_in_out"or"s_curve"are recommended for natural- sounding tempo changes. Seesubsequence.easingfor all available shapes.
Example:
# Accelerate to 140 BPM over the next 8 bars with a smooth S-curve comp.target_bpm(140, bars=8, shape="ease_in_out")
1792 def live_info (self) -> typing.Dict[str, typing.Any]: 1793 1794 """ 1795 Return a dictionary containing the current state of the composition. 1796 1797 Includes BPM, key, current bar, active section, current chord, 1798 running patterns, and custom data. 1799 """ 1800 1801 section_info = None 1802 if self._form_state is not None: 1803 section = self._form_state.get_section_info() 1804 if section is not None: 1805 section_info = { 1806 "name": section.name, 1807 "bar": section.bar, 1808 "bars": section.bars, 1809 "progress": section.progress 1810 } 1811 1812 chord_name = None 1813 if self._harmonic_state is not None: 1814 chord = self._harmonic_state.get_current_chord() 1815 if chord is not None: 1816 chord_name = chord.name() 1817 1818 pattern_list = [] 1819 channel_offset = 0 if self._zero_indexed_channels else 1 1820 for name, pat in self._running_patterns.items(): 1821 pattern_list.append({ 1822 "name": name, 1823 "channel": pat.channel + channel_offset, 1824 "length": pat.length, 1825 "cycle": pat._cycle_count, 1826 "muted": pat._muted, 1827 "tweaks": dict(pat._tweaks) 1828 }) 1829 1830 return { 1831 "bpm": self._sequencer.current_bpm, 1832 "key": self.key, 1833 "bar": self._builder_bar, 1834 "section": section_info, 1835 "chord": chord_name, 1836 "patterns": pattern_list, 1837 "input_device": self._input_device, 1838 "clock_follow": self.is_clock_following, 1839 "data": self.data 1840 }
Return a dictionary containing the current state of the composition.
Includes BPM, key, current bar, active section, current chord, running patterns, and custom data.
1842 def mute (self, name: str) -> None: 1843 1844 """ 1845 Mute a running pattern by name. 1846 1847 The pattern continues to 'run' and increment its cycle count in 1848 the background, but it will not produce any MIDI notes until unmuted. 1849 1850 Parameters: 1851 name: The function name of the pattern to mute. 1852 """ 1853 1854 if name not in self._running_patterns: 1855 raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}") 1856 1857 self._running_patterns[name]._muted = True 1858 logger.info(f"Muted pattern: {name}")
Mute a running pattern by name.
The pattern continues to 'run' and increment its cycle count in the background, but it will not produce any MIDI notes until unmuted.
Arguments:
- name: The function name of the pattern to mute.
1860 def unmute (self, name: str) -> None: 1861 1862 """ 1863 Unmute a previously muted pattern. 1864 """ 1865 1866 if name not in self._running_patterns: 1867 raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}") 1868 1869 self._running_patterns[name]._muted = False 1870 logger.info(f"Unmuted pattern: {name}")
Unmute a previously muted pattern.
1872 def tweak (self, name: str, **kwargs: typing.Any) -> None: 1873 1874 """Override parameters for a running pattern. 1875 1876 Values set here are available inside the pattern's builder 1877 function via ``p.param()``. They persist across rebuilds 1878 until explicitly changed or cleared. Changes take effect 1879 on the next rebuild cycle. 1880 1881 Parameters: 1882 name: The function name of the pattern. 1883 **kwargs: Parameter names and their new values. 1884 1885 Example (from the live REPL):: 1886 1887 composition.tweak("bass", pitches=[48, 52, 55, 60]) 1888 """ 1889 1890 if name not in self._running_patterns: 1891 raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}") 1892 1893 self._running_patterns[name]._tweaks.update(kwargs) 1894 logger.info(f"Tweaked pattern '{name}': {list(kwargs.keys())}")
Override parameters for a running pattern.
Values set here are available inside the pattern's builder
function via p.param(). They persist across rebuilds
until explicitly changed or cleared. Changes take effect
on the next rebuild cycle.
Arguments:
- name: The function name of the pattern.
- **kwargs: Parameter names and their new values.
Example (from the live REPL)::
composition.tweak("bass", pitches=[48, 52, 55, 60])
1896 def clear_tweak (self, name: str, *param_names: str) -> None: 1897 1898 """Remove tweaked parameters from a running pattern. 1899 1900 If no parameter names are given, all tweaks for the pattern 1901 are cleared and every ``p.param()`` call reverts to its 1902 default. 1903 1904 Parameters: 1905 name: The function name of the pattern. 1906 *param_names: Specific parameter names to clear. If 1907 omitted, all tweaks are removed. 1908 """ 1909 1910 if name not in self._running_patterns: 1911 raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}") 1912 1913 if not param_names: 1914 self._running_patterns[name]._tweaks.clear() 1915 logger.info(f"Cleared all tweaks for pattern '{name}'") 1916 else: 1917 for param_name in param_names: 1918 self._running_patterns[name]._tweaks.pop(param_name, None) 1919 logger.info(f"Cleared tweaks for pattern '{name}': {list(param_names)}")
Remove tweaked parameters from a running pattern.
If no parameter names are given, all tweaks for the pattern
are cleared and every p.param() call reverts to its
default.
Arguments:
- name: The function name of the pattern.
- *param_names: Specific parameter names to clear. If omitted, all tweaks are removed.
1921 def get_tweaks (self, name: str) -> typing.Dict[str, typing.Any]: 1922 1923 """Return a copy of the current tweaks for a running pattern. 1924 1925 Parameters: 1926 name: The function name of the pattern. 1927 """ 1928 1929 if name not in self._running_patterns: 1930 raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}") 1931 1932 return dict(self._running_patterns[name]._tweaks)
Return a copy of the current tweaks for a running pattern.
Arguments:
- name: The function name of the pattern.
1934 def schedule (self, fn: typing.Callable, cycle_beats: int, reschedule_lookahead: int = 1, wait_for_initial: bool = False, defer: bool = False) -> None: 1935 1936 """ 1937 Register a custom function to run on a repeating beat-based cycle. 1938 1939 Subsequence automatically runs synchronous functions in a thread pool 1940 so they don't block the timing-critical MIDI clock. Async functions 1941 are run directly on the event loop. 1942 1943 Parameters: 1944 fn: The function to call. 1945 cycle_beats: How often to call it (e.g., 4 = every bar). 1946 reschedule_lookahead: How far in advance to schedule the next call. 1947 wait_for_initial: If True, run the function once during startup 1948 and wait for it to complete before playback begins. This 1949 ensures ``composition.data`` is populated before patterns 1950 first build. Implies ``defer=True`` for the repeating 1951 schedule. 1952 defer: If True, skip the pulse-0 fire and defer the first 1953 repeating call to just before the second cycle boundary. 1954 """ 1955 1956 self._pending_scheduled.append(_PendingScheduled(fn, cycle_beats, reschedule_lookahead, wait_for_initial, defer))
Register a custom function to run on a repeating beat-based cycle.
Subsequence automatically runs synchronous functions in a thread pool so they don't block the timing-critical MIDI clock. Async functions are run directly on the event loop.
Arguments:
- fn: The function to call.
- cycle_beats: How often to call it (e.g., 4 = every bar).
- reschedule_lookahead: How far in advance to schedule the next call.
- wait_for_initial: If True, run the function once during startup
and wait for it to complete before playback begins. This
ensures
composition.datais populated before patterns first build. Impliesdefer=Truefor the repeating schedule. - defer: If True, skip the pulse-0 fire and defer the first repeating call to just before the second cycle boundary.
1958 def form ( 1959 self, 1960 sections: typing.Union[ 1961 typing.List[typing.Tuple[str, int]], 1962 typing.Iterator[typing.Tuple[str, int]], 1963 typing.Dict[str, typing.Tuple[int, typing.Optional[typing.List[typing.Tuple[str, int]]]]] 1964 ], 1965 loop: bool = False, 1966 start: typing.Optional[str] = None 1967 ) -> None: 1968 1969 """ 1970 Define the structure (sections) of the composition. 1971 1972 You can define form in three ways: 1973 1. **Graph (Dict)**: Dynamic transitions based on weights. 1974 2. **Sequence (List)**: A fixed order of sections. 1975 3. **Generator**: A Python generator that yields `(name, bars)` pairs. 1976 1977 Parameters: 1978 sections: The form definition (Dict, List, or Generator). 1979 loop: Whether to cycle back to the start (List mode only). 1980 start: The section to start with (Graph mode only). 1981 1982 Example: 1983 ```python 1984 # A simple pop structure 1985 comp.form([ 1986 ("verse", 8), 1987 ("chorus", 8), 1988 ("verse", 8), 1989 ("chorus", 16) 1990 ]) 1991 ``` 1992 """ 1993 1994 self._form_state = subsequence.form_state.FormState(sections, loop=loop, start=start)
Define the structure (sections) of the composition.
You can define form in three ways:
- Graph (Dict): Dynamic transitions based on weights.
- Sequence (List): A fixed order of sections.
- Generator: A Python generator that yields
(name, bars)pairs.
Arguments:
- sections: The form definition (Dict, List, or Generator).
- loop: Whether to cycle back to the start (List mode only).
- start: The section to start with (Graph mode only).
Example:
# A simple pop structure comp.form([ ("verse", 8), ("chorus", 8), ("verse", 8), ("chorus", 16) ])
2047 def pattern ( 2048 self, 2049 channel: int, 2050 beats: typing.Optional[float] = None, 2051 bars: typing.Optional[float] = None, 2052 steps: typing.Optional[float] = None, 2053 unit: typing.Optional[float] = None, 2054 drum_note_map: typing.Optional[typing.Dict[str, int]] = None, 2055 cc_name_map: typing.Optional[typing.Dict[str, int]] = None, 2056 reschedule_lookahead: float = 1, 2057 voice_leading: bool = False, 2058 device: subsequence.midi_utils.DeviceId = None, 2059 ) -> typing.Callable: 2060 2061 """ 2062 Register a function as a repeating MIDI pattern. 2063 2064 The decorated function will be called once per cycle to 'rebuild' its 2065 content. This allows for generative logic that evolves over time. 2066 2067 Two ways to specify pattern length: 2068 2069 - **Duration mode** (default): use ``beats=`` or ``bars=``. 2070 The grid defaults to sixteenth-note resolution. 2071 - **Step mode**: use ``steps=`` paired with ``unit=``. 2072 The grid equals the step count, so ``p.hit_steps()`` indices map 2073 directly to steps. 2074 2075 Parameters: 2076 channel: MIDI channel. By default uses 0-based numbering (0-15) 2077 matching the raw MIDI protocol. Set 2078 ``zero_indexed_channels=False`` on the ``Composition`` to use 2079 1-based numbering (1-16) instead. 2080 beats: Duration in beats (quarter notes). ``beats=4`` = 1 bar. 2081 bars: Duration in bars (4 beats each, assumes 4/4). ``bars=2`` = 8 beats. 2082 steps: Step count for step mode. Requires ``unit=``. 2083 unit: Duration of one step in beats (e.g. ``dur.SIXTEENTH``). 2084 Requires ``steps=``. 2085 drum_note_map: Optional mapping for drum instruments. 2086 cc_name_map: Optional mapping of CC names to MIDI CC numbers. 2087 Enables string-based CC names in ``p.cc()`` and ``p.cc_ramp()``. 2088 reschedule_lookahead: Beats in advance to compute the next cycle. 2089 voice_leading: If True, chords in this pattern will automatically 2090 use inversions that minimize voice movement. 2091 2092 Example: 2093 ```python 2094 @comp.pattern(channel=1, beats=4) 2095 def chords (p): 2096 p.chord([60, 64, 67], beat=0, velocity=80, duration=3.9) 2097 2098 @comp.pattern(channel=1, bars=2) 2099 def long_phrase (p): 2100 ... 2101 2102 @comp.pattern(channel=1, steps=6, unit=dur.SIXTEENTH) 2103 def riff (p): 2104 p.sequence(steps=[0, 1, 3, 5], pitches=60) 2105 ``` 2106 """ 2107 2108 channel = self._resolve_channel(channel) 2109 2110 beat_length, default_grid = self._resolve_length(beats, bars, steps, unit) 2111 2112 # Resolve device string name to index if possible now; otherwise store 2113 # the raw DeviceId and resolve it in _run() once all devices are open. 2114 resolved_device: subsequence.midi_utils.DeviceId = device 2115 2116 def decorator (fn: typing.Callable) -> typing.Callable: 2117 2118 """ 2119 Wrap the builder function and register it as a pending pattern. 2120 During live sessions, hot-swap an existing pattern's builder instead. 2121 """ 2122 2123 # Hot-swap: if we're live and a pattern with this name exists, replace its builder. 2124 if self._is_live and fn.__name__ in self._running_patterns: 2125 running = self._running_patterns[fn.__name__] 2126 running._builder_fn = fn 2127 running._wants_chord = _fn_has_parameter(fn, "chord") 2128 logger.info(f"Hot-swapped pattern: {fn.__name__}") 2129 return fn 2130 2131 pending = _PendingPattern( 2132 builder_fn = fn, 2133 channel = channel, # already resolved to 0-indexed 2134 length = beat_length, 2135 default_grid = default_grid, 2136 drum_note_map = drum_note_map, 2137 cc_name_map = cc_name_map, 2138 reschedule_lookahead = reschedule_lookahead, 2139 voice_leading = voice_leading, 2140 # For int/None: resolve immediately. For str: store 0 as 2141 # placeholder; _resolve_pending_devices() fixes it in _run(). 2142 device = 0 if (resolved_device is None or isinstance(resolved_device, str)) else resolved_device, 2143 raw_device = resolved_device, 2144 ) 2145 2146 self._pending_patterns.append(pending) 2147 2148 return fn 2149 2150 return decorator
Register a function as a repeating MIDI pattern.
The decorated function will be called once per cycle to 'rebuild' its content. This allows for generative logic that evolves over time.
Two ways to specify pattern length:
- Duration mode (default): use
beats=orbars=. The grid defaults to sixteenth-note resolution. - Step mode: use
steps=paired withunit=. The grid equals the step count, sop.hit_steps()indices map directly to steps.
Arguments:
- channel: MIDI channel. By default uses 0-based numbering (0-15)
matching the raw MIDI protocol. Set
zero_indexed_channels=Falseon theCompositionto use 1-based numbering (1-16) instead. - beats: Duration in beats (quarter notes).
beats=4= 1 bar. - bars: Duration in bars (4 beats each, assumes 4/4).
bars=2= 8 beats. - steps: Step count for step mode. Requires
unit=. - unit: Duration of one step in beats (e.g.
dur.SIXTEENTH). Requiressteps=. - drum_note_map: Optional mapping for drum instruments.
- cc_name_map: Optional mapping of CC names to MIDI CC numbers.
Enables string-based CC names in
p.cc()andp.cc_ramp(). - reschedule_lookahead: Beats in advance to compute the next cycle.
- voice_leading: If True, chords in this pattern will automatically use inversions that minimize voice movement.
Example:
@comp.pattern(channel=1, beats=4) def chords (p): p.chord([60, 64, 67], beat=0, velocity=80, duration=3.9) @comp.pattern(channel=1, bars=2) def long_phrase (p): ... @comp.pattern(channel=1, steps=6, unit=dur.SIXTEENTH) def riff (p): p.sequence(steps=[0, 1, 3, 5], pitches=60)
2152 def layer ( 2153 self, 2154 *builder_fns: typing.Callable, 2155 channel: int, 2156 beats: typing.Optional[float] = None, 2157 bars: typing.Optional[float] = None, 2158 steps: typing.Optional[float] = None, 2159 unit: typing.Optional[float] = None, 2160 drum_note_map: typing.Optional[typing.Dict[str, int]] = None, 2161 cc_name_map: typing.Optional[typing.Dict[str, int]] = None, 2162 reschedule_lookahead: float = 1, 2163 voice_leading: bool = False, 2164 device: subsequence.midi_utils.DeviceId = None, 2165 ) -> None: 2166 2167 """ 2168 Combine multiple functions into a single MIDI pattern. 2169 2170 This is useful for composing complex patterns out of reusable 2171 building blocks (e.g., a 'kick' function and a 'snare' function). 2172 2173 See ``pattern()`` for the full description of ``beats``, ``bars``, 2174 ``steps``, and ``unit``. 2175 2176 Parameters: 2177 builder_fns: One or more pattern builder functions. 2178 channel: MIDI channel (0-15, or 1-16 with ``zero_indexed_channels=False``). 2179 beats: Duration in beats (quarter notes). 2180 bars: Duration in bars (4 beats each, assumes 4/4). 2181 steps: Step count for step mode. Requires ``unit=``. 2182 unit: Duration of one step in beats. Requires ``steps=``. 2183 drum_note_map: Optional mapping for drum instruments. 2184 cc_name_map: Optional mapping of CC names to MIDI CC numbers. 2185 reschedule_lookahead: Beats in advance to compute the next cycle. 2186 voice_leading: If True, chords use smooth voice leading. 2187 """ 2188 2189 beat_length, default_grid = self._resolve_length(beats, bars, steps, unit) 2190 2191 wants_chord = any(_fn_has_parameter(fn, "chord") for fn in builder_fns) 2192 2193 if wants_chord: 2194 2195 def merged_builder (p: subsequence.pattern_builder.PatternBuilder, chord: _InjectedChord) -> None: 2196 2197 for fn in builder_fns: 2198 if _fn_has_parameter(fn, "chord"): 2199 fn(p, chord) 2200 else: 2201 fn(p) 2202 2203 else: 2204 2205 def merged_builder (p: subsequence.pattern_builder.PatternBuilder) -> None: # type: ignore[misc] 2206 2207 for fn in builder_fns: 2208 fn(p) 2209 2210 resolved = self._resolve_channel(channel) 2211 2212 pending = _PendingPattern( 2213 builder_fn = merged_builder, 2214 channel = resolved, # already resolved to 0-indexed 2215 length = beat_length, 2216 default_grid = default_grid, 2217 drum_note_map = drum_note_map, 2218 cc_name_map = cc_name_map, 2219 reschedule_lookahead = reschedule_lookahead, 2220 voice_leading = voice_leading, 2221 device = 0 if (device is None or isinstance(device, str)) else device, 2222 raw_device = device, 2223 ) 2224 2225 self._pending_patterns.append(pending)
Combine multiple functions into a single MIDI pattern.
This is useful for composing complex patterns out of reusable building blocks (e.g., a 'kick' function and a 'snare' function).
See pattern() for the full description of beats, bars,
steps, and unit.
Arguments:
- builder_fns: One or more pattern builder functions.
- channel: MIDI channel (0-15, or 1-16 with
zero_indexed_channels=False). - beats: Duration in beats (quarter notes).
- bars: Duration in bars (4 beats each, assumes 4/4).
- steps: Step count for step mode. Requires
unit=. - unit: Duration of one step in beats. Requires
steps=. - drum_note_map: Optional mapping for drum instruments.
- cc_name_map: Optional mapping of CC names to MIDI CC numbers.
- reschedule_lookahead: Beats in advance to compute the next cycle.
- voice_leading: If True, chords use smooth voice leading.
2227 def trigger ( 2228 self, 2229 fn: typing.Callable, 2230 channel: int, 2231 beats: typing.Optional[float] = None, 2232 bars: typing.Optional[float] = None, 2233 steps: typing.Optional[float] = None, 2234 unit: typing.Optional[float] = None, 2235 quantize: float = 0, 2236 drum_note_map: typing.Optional[typing.Dict[str, int]] = None, 2237 cc_name_map: typing.Optional[typing.Dict[str, int]] = None, 2238 chord: bool = False, 2239 device: subsequence.midi_utils.DeviceId = None, 2240 ) -> None: 2241 2242 """ 2243 Trigger a one-shot pattern immediately or on a quantized boundary. 2244 2245 This is useful for real-time response to sensors, OSC messages, or other 2246 external events. The builder function is called immediately with a fresh 2247 PatternBuilder, and the generated events are injected into the queue at 2248 the specified quantize boundary. 2249 2250 The builder function has the same API as a ``@composition.pattern`` 2251 decorated function and can use all PatternBuilder methods: ``p.note()``, 2252 ``p.euclidean()``, ``p.arpeggio()``, and so on. 2253 2254 See ``pattern()`` for the full description of ``beats``, ``bars``, 2255 ``steps``, and ``unit``. Default is 1 beat. 2256 2257 Parameters: 2258 fn: The pattern builder function (same signature as ``@comp.pattern``). 2259 channel: MIDI channel (0-15, or 1-16 with ``zero_indexed_channels=False``). 2260 beats: Duration in beats (quarter notes, default 1). 2261 bars: Duration in bars (4 beats each, assumes 4/4). 2262 steps: Step count for step mode. Requires ``unit=``. 2263 unit: Duration of one step in beats. Requires ``steps=``. 2264 quantize: Snap the trigger to a beat boundary: ``0`` = immediate (default), 2265 ``1`` = next beat (quarter note), ``4`` = next bar. Use ``dur.*`` 2266 constants from ``subsequence.constants.durations``. 2267 drum_note_map: Optional drum name mapping for this pattern. 2268 cc_name_map: Optional mapping of CC names to MIDI CC numbers. 2269 chord: If ``True``, the builder function receives the current chord as 2270 a second parameter (same as ``@composition.pattern``). 2271 2272 Example: 2273 ```python 2274 # Immediate single note 2275 composition.trigger( 2276 lambda p: p.note(60, beat=0, velocity=100, duration=0.5), 2277 channel=0 2278 ) 2279 2280 # Quantized fill (next bar) 2281 import subsequence.constants.durations as dur 2282 composition.trigger( 2283 lambda p: p.euclidean("snare", pulses=7, velocity=90), 2284 channel=9, 2285 drum_note_map=gm_drums.GM_DRUM_MAP, 2286 quantize=dur.WHOLE 2287 ) 2288 2289 # With chord context 2290 composition.trigger( 2291 lambda p: p.arpeggio(p.chord.tones(root=60), spacing=dur.SIXTEENTH), 2292 channel=0, 2293 quantize=dur.QUARTER, 2294 chord=True 2295 ) 2296 ``` 2297 """ 2298 2299 # Resolve channel numbering 2300 resolved_channel = self._resolve_channel(channel) 2301 2302 beat_length, default_grid = self._resolve_length(beats, bars, steps, unit, default=1.0) 2303 2304 # Resolve device index 2305 resolved_device_idx = self._resolve_device_id(device) 2306 2307 # Create a temporary Pattern 2308 pattern = subsequence.pattern.Pattern(channel=resolved_channel, length=beat_length, device=resolved_device_idx) 2309 2310 # Create a PatternBuilder 2311 builder = subsequence.pattern_builder.PatternBuilder( 2312 pattern=pattern, 2313 cycle=0, # One-shot patterns don't rebuild, so cycle is always 0 2314 drum_note_map=drum_note_map, 2315 cc_name_map=cc_name_map, 2316 section=self._form_state.get_section_info() if self._form_state else None, 2317 bar=self._builder_bar, 2318 conductor=self.conductor, 2319 rng=random.Random(), # Fresh random state for each trigger 2320 tweaks={}, 2321 default_grid=default_grid, 2322 data=self.data 2323 ) 2324 2325 # Call the builder function 2326 try: 2327 2328 if chord and self._harmonic_state is not None: 2329 current_chord = self._harmonic_state.get_current_chord() 2330 injected = _InjectedChord(current_chord, None) # No voice leading for one-shots 2331 fn(builder, injected) 2332 2333 else: 2334 fn(builder) 2335 2336 except Exception: 2337 logger.exception("Error in trigger builder — pattern will be silent") 2338 return 2339 2340 # Calculate the start pulse based on quantize 2341 current_pulse = self._sequencer.pulse_count 2342 pulses_per_beat = subsequence.constants.MIDI_QUARTER_NOTE 2343 2344 if quantize == 0: 2345 # Immediate: use current pulse 2346 start_pulse = current_pulse 2347 2348 else: 2349 # Quantize to the next multiple of (quantize * pulses_per_beat) 2350 quantize_pulses = int(quantize * pulses_per_beat) 2351 start_pulse = ((current_pulse // quantize_pulses) + 1) * quantize_pulses 2352 2353 # Schedule the pattern for one-shot execution 2354 try: 2355 loop = asyncio.get_running_loop() 2356 # Already on the event loop 2357 asyncio.create_task(self._sequencer.schedule_pattern(pattern, start_pulse)) 2358 2359 except RuntimeError: 2360 # Not on the event loop — schedule via call_soon_threadsafe 2361 if self._sequencer._event_loop is not None: 2362 asyncio.run_coroutine_threadsafe( 2363 self._sequencer.schedule_pattern(pattern, start_pulse), 2364 loop=self._sequencer._event_loop 2365 ) 2366 else: 2367 logger.warning("trigger() called before playback started; pattern ignored")
Trigger a one-shot pattern immediately or on a quantized boundary.
This is useful for real-time response to sensors, OSC messages, or other external events. The builder function is called immediately with a fresh PatternBuilder, and the generated events are injected into the queue at the specified quantize boundary.
The builder function has the same API as a @composition.pattern
decorated function and can use all PatternBuilder methods: p.note(),
p.euclidean(), p.arpeggio(), and so on.
See pattern() for the full description of beats, bars,
steps, and unit. Default is 1 beat.
Arguments:
- fn: The pattern builder function (same signature as
@comp.pattern). - channel: MIDI channel (0-15, or 1-16 with
zero_indexed_channels=False). - beats: Duration in beats (quarter notes, default 1).
- bars: Duration in bars (4 beats each, assumes 4/4).
- steps: Step count for step mode. Requires
unit=. - unit: Duration of one step in beats. Requires
steps=. - quantize: Snap the trigger to a beat boundary:
0= immediate (default),1= next beat (quarter note),4= next bar. Usedur.*constants fromsubsequence.constants.durations. - drum_note_map: Optional drum name mapping for this pattern.
- cc_name_map: Optional mapping of CC names to MIDI CC numbers.
- chord: If
True, the builder function receives the current chord as a second parameter (same as@composition.pattern).
Example:
# Immediate single note composition.trigger( lambda p: p.note(60, beat=0, velocity=100, duration=0.5), channel=0 ) # Quantized fill (next bar) import subsequence.constants.durations as dur composition.trigger( lambda p: p.euclidean("snare", pulses=7, velocity=90), channel=9, drum_note_map=gm_drums.GM_DRUM_MAP, quantize=dur.WHOLE ) # With chord context composition.trigger( lambda p: p.arpeggio(p.chord.tones(root=60), spacing=dur.SIXTEENTH), channel=0, quantize=dur.QUARTER, chord=True )
2369 @property 2370 def is_clock_following (self) -> bool: 2371 2372 """True if either the primary or any additional device is following external clock.""" 2373 2374 return self._clock_follow or any(cf for _, _, cf in self._additional_inputs)
True if either the primary or any additional device is following external clock.
2377 def play (self) -> None: 2378 2379 """ 2380 Start the composition. 2381 2382 This call blocks until the program is interrupted (e.g., via Ctrl+C). 2383 It initializes the MIDI hardware, launches the background sequencer, 2384 and begins playback. 2385 """ 2386 2387 try: 2388 asyncio.run(self._run()) 2389 2390 except KeyboardInterrupt: 2391 pass
Start the composition.
This call blocks until the program is interrupted (e.g., via Ctrl+C). It initializes the MIDI hardware, launches the background sequencer, and begins playback.
2394 def render (self, bars: typing.Optional[int] = None, filename: str = "render.mid", max_minutes: typing.Optional[float] = 60.0) -> None: 2395 2396 """Render the composition to a MIDI file without real-time playback. 2397 2398 Runs the sequencer as fast as possible (no timing delays) and stops 2399 when the first active limit is reached. The result is saved as a 2400 standard MIDI file that can be imported into any DAW. 2401 2402 All patterns, scheduled callbacks, and harmony logic run exactly as 2403 they would during live playback — BPM transitions, generative fills, 2404 and probabilistic gates all work in render mode. The only difference 2405 is that time is simulated rather than wall-clock driven. 2406 2407 Parameters: 2408 bars: Number of bars to render, or ``None`` for no bar limit 2409 (default ``None``). When both *bars* and *max_minutes* are 2410 active, playback stops at whichever limit is reached first. 2411 filename: Output MIDI filename (default ``"render.mid"``). 2412 max_minutes: Safety cap on the length of rendered MIDI in minutes 2413 (default ``60.0``). Pass ``None`` to disable the time 2414 cap — you must then provide an explicit *bars* value. 2415 2416 Raises: 2417 ValueError: If both *bars* and *max_minutes* are ``None``, which 2418 would produce an infinite render. 2419 2420 Examples: 2421 ```python 2422 # Default: renders up to 60 minutes of MIDI content. 2423 composition.render() 2424 2425 # Render exactly 64 bars (time cap still active as backstop). 2426 composition.render(bars=64, filename="demo.mid") 2427 2428 # Render up to 5 minutes of an infinite generative composition. 2429 composition.render(max_minutes=5, filename="five_min.mid") 2430 2431 # Remove the time cap — must supply bars instead. 2432 composition.render(bars=128, max_minutes=None, filename="long.mid") 2433 ``` 2434 """ 2435 2436 if bars is None and max_minutes is None: 2437 raise ValueError( 2438 "render() requires at least one limit: provide bars=, max_minutes=, or both. " 2439 "Passing both as None would produce an infinite render." 2440 ) 2441 2442 self._sequencer.recording = True 2443 self._sequencer.record_filename = filename 2444 self._sequencer.render_mode = True 2445 self._sequencer.render_bars = bars if bars is not None else 0 2446 self._sequencer.render_max_seconds = max_minutes * 60.0 if max_minutes is not None else None 2447 asyncio.run(self._run())
Render the composition to a MIDI file without real-time playback.
Runs the sequencer as fast as possible (no timing delays) and stops when the first active limit is reached. The result is saved as a standard MIDI file that can be imported into any DAW.
All patterns, scheduled callbacks, and harmony logic run exactly as they would during live playback — BPM transitions, generative fills, and probabilistic gates all work in render mode. The only difference is that time is simulated rather than wall-clock driven.
Arguments:
- bars: Number of bars to render, or
Nonefor no bar limit (defaultNone). When both bars and max_minutes are active, playback stops at whichever limit is reached first. - filename: Output MIDI filename (default
"render.mid"). - max_minutes: Safety cap on the length of rendered MIDI in minutes
(default
60.0). PassNoneto disable the time cap — you must then provide an explicit bars value.
Raises:
- ValueError: If both bars and max_minutes are
None, which would produce an infinite render.
Examples:
# Default: renders up to 60 minutes of MIDI content. composition.render() # Render exactly 64 bars (time cap still active as backstop). composition.render(bars=64, filename="demo.mid") # Render up to 5 minutes of an infinite generative composition. composition.render(max_minutes=5, filename="five_min.mid") # Remove the time cap — must supply bars instead. composition.render(bars=128, max_minutes=None, filename="long.mid")
14@dataclasses.dataclass 15class Groove: 16 17 """ 18 A timing/velocity template applied to quantized grid positions. 19 20 A groove is a repeating pattern of per-step timing offsets and optional 21 velocity adjustments aligned to a rhythmic grid. Apply it as a post-build 22 transform with ``p.groove(template)`` to give a pattern its characteristic 23 feel — swing, shuffle, MPC-style pocket, or anything extracted from an 24 Ableton ``.agr`` file. 25 26 Parameters: 27 offsets: Timing offset per grid slot, in beats. Repeats cyclically. 28 Positive values delay the note; negative values push it earlier. 29 grid: Grid size in beats (0.25 = 16th notes, 0.5 = 8th notes). 30 velocities: Optional velocity scale per grid slot (1.0 = unchanged). 31 Repeats cyclically alongside offsets. 32 33 Example:: 34 35 # Ableton-style 57% swing on 16th notes 36 groove = Groove.swing(percent=57) 37 38 # Custom groove with timing and velocity 39 groove = Groove( 40 grid=0.25, 41 offsets=[0.0, +0.02, 0.0, -0.01], 42 velocities=[1.0, 0.7, 0.9, 0.6], 43 ) 44 """ 45 46 offsets: typing.List[float] 47 grid: float = 0.25 48 velocities: typing.Optional[typing.List[float]] = None 49 50 def __post_init__ (self) -> None: 51 if not self.offsets: 52 raise ValueError("offsets must not be empty") 53 if self.grid <= 0: 54 raise ValueError("grid must be positive") 55 if self.velocities is not None and not self.velocities: 56 raise ValueError("velocities must not be empty (use None for no velocity adjustment)") 57 58 @staticmethod 59 def swing (percent: float = 57.0, grid: float = 0.25) -> "Groove": 60 61 """ 62 Create a swing groove from a percentage. 63 64 50% is straight (no swing). 67% is approximately triplet swing. 65 57% is a moderate shuffle — the Ableton default. 66 67 Parameters: 68 percent: Swing amount (50–75 is the useful range). 69 grid: Grid size in beats (0.25 = 16ths, 0.5 = 8ths). 70 """ 71 72 if percent < 50.0 or percent > 99.0: 73 raise ValueError("swing percent must be between 50 and 99") 74 pair_duration = grid * 2 75 offset = (percent / 100.0 - 0.5) * pair_duration 76 return Groove(offsets=[0.0, offset], grid=grid) 77 78 @staticmethod 79 def from_agr (path: str) -> "Groove": 80 81 """ 82 Import timing and velocity data from an Ableton .agr groove file. 83 84 An ``.agr`` file is an XML document containing a MIDI clip whose 85 note positions encode the groove's rhythmic feel. This method reads 86 those note start times and velocities and converts them into the 87 ``Groove`` dataclass format (per-step offsets and velocity scales). 88 89 **What is extracted:** 90 91 - ``Time`` attribute of each ``MidiNoteEvent`` → timing offsets 92 relative to ideal grid positions. 93 - ``Velocity`` attribute of each ``MidiNoteEvent`` → velocity 94 scaling (normalised to the highest velocity in the file). 95 - ``TimingAmount`` from the Groove element → pre-scales the timing 96 offsets (100 = full, 70 = 70% of the groove's timing). 97 - ``VelocityAmount`` from the Groove element → pre-scales velocity 98 deviation (100 = full groove velocity, 0 = no velocity changes). 99 100 The resulting ``Groove`` reflects the file author's intended 101 strength. Use ``strength=`` when applying to further adjust. 102 103 **What is NOT imported:** 104 105 ``RandomAmount`` (use ``p.randomize()`` separately for random 106 jitter) and ``QuantizationAmount`` (not applicable - Subsequence 107 notes are already grid-quantized by construction). 108 109 Other ``MidiNoteEvent`` fields (``Duration``, ``VelocityDeviation``, 110 ``OffVelocity``, ``Probability``) are also ignored. 111 112 Parameters: 113 path: Path to the .agr file. 114 """ 115 116 tree = xml.etree.ElementTree.parse(path) 117 root = tree.getroot() 118 119 # Find the MIDI clip 120 clip = root.find(".//MidiClip") 121 if clip is None: 122 raise ValueError(f"No MidiClip found in {path}") 123 124 # Get clip length 125 current_end = clip.find("CurrentEnd") 126 if current_end is None: 127 raise ValueError(f"No CurrentEnd found in {path}") 128 clip_length = float(current_end.get("Value", "4")) 129 130 # Read Groove Pool blend parameters 131 groove_elem = root.find(".//Groove") 132 timing_amount = 100.0 133 velocity_amount = 100.0 134 if groove_elem is not None: 135 timing_el = groove_elem.find("TimingAmount") 136 if timing_el is not None: 137 timing_amount = float(timing_el.get("Value", "100")) 138 velocity_el = groove_elem.find("VelocityAmount") 139 if velocity_el is not None: 140 velocity_amount = float(velocity_el.get("Value", "100")) 141 142 timing_scale = timing_amount / 100.0 143 velocity_scale = velocity_amount / 100.0 144 145 # Extract note events sorted by time 146 events = clip.findall(".//MidiNoteEvent") 147 if not events: 148 raise ValueError(f"No MidiNoteEvent elements found in {path}") 149 150 times: typing.List[float] = [] 151 velocities_raw: typing.List[float] = [] 152 for event in events: 153 times.append(float(event.get("Time", "0"))) 154 velocities_raw.append(float(event.get("Velocity", "127"))) 155 156 times.sort() 157 note_count = len(times) 158 159 # Infer grid from clip length and note count 160 grid = clip_length / note_count 161 162 # Calculate offsets from ideal grid positions, scaled by TimingAmount 163 offsets: typing.List[float] = [] 164 for i, time in enumerate(times): 165 ideal = i * grid 166 offsets.append((time - ideal) * timing_scale) 167 168 # Calculate velocity scales (relative to max velocity in the file), 169 # blended toward 1.0 by VelocityAmount 170 max_vel = max(velocities_raw) 171 has_velocity_variation = any(v != max_vel for v in velocities_raw) 172 groove_velocities: typing.Optional[typing.List[float]] = None 173 if has_velocity_variation and max_vel > 0: 174 raw_scales = [v / max_vel for v in velocities_raw] 175 # velocity_scale=1.0 → full groove velocity; 0.0 → all 1.0 (no change) 176 groove_velocities = [1.0 + (s - 1.0) * velocity_scale for s in raw_scales] 177 # If blending has removed all variation, set to None 178 if all(abs(v - 1.0) < 1e-9 for v in groove_velocities): 179 groove_velocities = None 180 181 return Groove(offsets=offsets, grid=grid, velocities=groove_velocities)
A timing/velocity template applied to quantized grid positions.
A groove is a repeating pattern of per-step timing offsets and optional
velocity adjustments aligned to a rhythmic grid. Apply it as a post-build
transform with p.groove(template) to give a pattern its characteristic
feel — swing, shuffle, MPC-style pocket, or anything extracted from an
Ableton .agr file.
Arguments:
- offsets: Timing offset per grid slot, in beats. Repeats cyclically. Positive values delay the note; negative values push it earlier.
- grid: Grid size in beats (0.25 = 16th notes, 0.5 = 8th notes).
- velocities: Optional velocity scale per grid slot (1.0 = unchanged). Repeats cyclically alongside offsets.
Example::
# Ableton-style 57% swing on 16th notes
groove = Groove.swing(percent=57)
# Custom groove with timing and velocity
groove = Groove(
grid=0.25,
offsets=[0.0, +0.02, 0.0, -0.01],
velocities=[1.0, 0.7, 0.9, 0.6],
)
58 @staticmethod 59 def swing (percent: float = 57.0, grid: float = 0.25) -> "Groove": 60 61 """ 62 Create a swing groove from a percentage. 63 64 50% is straight (no swing). 67% is approximately triplet swing. 65 57% is a moderate shuffle — the Ableton default. 66 67 Parameters: 68 percent: Swing amount (50–75 is the useful range). 69 grid: Grid size in beats (0.25 = 16ths, 0.5 = 8ths). 70 """ 71 72 if percent < 50.0 or percent > 99.0: 73 raise ValueError("swing percent must be between 50 and 99") 74 pair_duration = grid * 2 75 offset = (percent / 100.0 - 0.5) * pair_duration 76 return Groove(offsets=[0.0, offset], grid=grid)
Create a swing groove from a percentage.
50% is straight (no swing). 67% is approximately triplet swing. 57% is a moderate shuffle — the Ableton default.
Arguments:
- percent: Swing amount (50–75 is the useful range).
- grid: Grid size in beats (0.25 = 16ths, 0.5 = 8ths).
78 @staticmethod 79 def from_agr (path: str) -> "Groove": 80 81 """ 82 Import timing and velocity data from an Ableton .agr groove file. 83 84 An ``.agr`` file is an XML document containing a MIDI clip whose 85 note positions encode the groove's rhythmic feel. This method reads 86 those note start times and velocities and converts them into the 87 ``Groove`` dataclass format (per-step offsets and velocity scales). 88 89 **What is extracted:** 90 91 - ``Time`` attribute of each ``MidiNoteEvent`` → timing offsets 92 relative to ideal grid positions. 93 - ``Velocity`` attribute of each ``MidiNoteEvent`` → velocity 94 scaling (normalised to the highest velocity in the file). 95 - ``TimingAmount`` from the Groove element → pre-scales the timing 96 offsets (100 = full, 70 = 70% of the groove's timing). 97 - ``VelocityAmount`` from the Groove element → pre-scales velocity 98 deviation (100 = full groove velocity, 0 = no velocity changes). 99 100 The resulting ``Groove`` reflects the file author's intended 101 strength. Use ``strength=`` when applying to further adjust. 102 103 **What is NOT imported:** 104 105 ``RandomAmount`` (use ``p.randomize()`` separately for random 106 jitter) and ``QuantizationAmount`` (not applicable - Subsequence 107 notes are already grid-quantized by construction). 108 109 Other ``MidiNoteEvent`` fields (``Duration``, ``VelocityDeviation``, 110 ``OffVelocity``, ``Probability``) are also ignored. 111 112 Parameters: 113 path: Path to the .agr file. 114 """ 115 116 tree = xml.etree.ElementTree.parse(path) 117 root = tree.getroot() 118 119 # Find the MIDI clip 120 clip = root.find(".//MidiClip") 121 if clip is None: 122 raise ValueError(f"No MidiClip found in {path}") 123 124 # Get clip length 125 current_end = clip.find("CurrentEnd") 126 if current_end is None: 127 raise ValueError(f"No CurrentEnd found in {path}") 128 clip_length = float(current_end.get("Value", "4")) 129 130 # Read Groove Pool blend parameters 131 groove_elem = root.find(".//Groove") 132 timing_amount = 100.0 133 velocity_amount = 100.0 134 if groove_elem is not None: 135 timing_el = groove_elem.find("TimingAmount") 136 if timing_el is not None: 137 timing_amount = float(timing_el.get("Value", "100")) 138 velocity_el = groove_elem.find("VelocityAmount") 139 if velocity_el is not None: 140 velocity_amount = float(velocity_el.get("Value", "100")) 141 142 timing_scale = timing_amount / 100.0 143 velocity_scale = velocity_amount / 100.0 144 145 # Extract note events sorted by time 146 events = clip.findall(".//MidiNoteEvent") 147 if not events: 148 raise ValueError(f"No MidiNoteEvent elements found in {path}") 149 150 times: typing.List[float] = [] 151 velocities_raw: typing.List[float] = [] 152 for event in events: 153 times.append(float(event.get("Time", "0"))) 154 velocities_raw.append(float(event.get("Velocity", "127"))) 155 156 times.sort() 157 note_count = len(times) 158 159 # Infer grid from clip length and note count 160 grid = clip_length / note_count 161 162 # Calculate offsets from ideal grid positions, scaled by TimingAmount 163 offsets: typing.List[float] = [] 164 for i, time in enumerate(times): 165 ideal = i * grid 166 offsets.append((time - ideal) * timing_scale) 167 168 # Calculate velocity scales (relative to max velocity in the file), 169 # blended toward 1.0 by VelocityAmount 170 max_vel = max(velocities_raw) 171 has_velocity_variation = any(v != max_vel for v in velocities_raw) 172 groove_velocities: typing.Optional[typing.List[float]] = None 173 if has_velocity_variation and max_vel > 0: 174 raw_scales = [v / max_vel for v in velocities_raw] 175 # velocity_scale=1.0 → full groove velocity; 0.0 → all 1.0 (no change) 176 groove_velocities = [1.0 + (s - 1.0) * velocity_scale for s in raw_scales] 177 # If blending has removed all variation, set to None 178 if all(abs(v - 1.0) < 1e-9 for v in groove_velocities): 179 groove_velocities = None 180 181 return Groove(offsets=offsets, grid=grid, velocities=groove_velocities)
Import timing and velocity data from an Ableton .agr groove file.
An .agr file is an XML document containing a MIDI clip whose
note positions encode the groove's rhythmic feel. This method reads
those note start times and velocities and converts them into the
Groove dataclass format (per-step offsets and velocity scales).
What is extracted:
Timeattribute of eachMidiNoteEvent→ timing offsets relative to ideal grid positions.Velocityattribute of eachMidiNoteEvent→ velocity scaling (normalised to the highest velocity in the file).TimingAmountfrom the Groove element → pre-scales the timing offsets (100 = full, 70 = 70% of the groove's timing).VelocityAmountfrom the Groove element → pre-scales velocity deviation (100 = full groove velocity, 0 = no velocity changes).
The resulting Groove reflects the file author's intended
strength. Use strength= when applying to further adjust.
What is NOT imported:
RandomAmount (use p.randomize() separately for random
jitter) and QuantizationAmount (not applicable - Subsequence
notes are already grid-quantized by construction).
Other MidiNoteEvent fields (Duration, VelocityDeviation,
OffVelocity, Probability) are also ignored.
Arguments:
- path: Path to the .agr file.
24class MelodicState: 25 26 """Persistent melodic context that applies NIR scoring to single-note lines.""" 27 28 29 def __init__ ( 30 self, 31 key: str = "C", 32 mode: str = "ionian", 33 low: int = 48, 34 high: int = 72, 35 nir_strength: float = 0.5, 36 chord_weight: float = 0.4, 37 rest_probability: float = 0.0, 38 pitch_diversity: float = 0.6, 39 ) -> None: 40 41 """Initialise a melodic state for a given key, mode, and MIDI register. 42 43 Parameters: 44 key: Root note of the key (e.g. ``"C"``, ``"F#"``, ``"Bb"``). 45 mode: Scale mode name. Accepts any mode registered with 46 :func:`~subsequence.intervals.scale_pitch_classes` (e.g. 47 ``"ionian"``, ``"aeolian"``, ``"dorian"``). 48 low: Lowest MIDI note (inclusive) in the pitch pool. 49 high: Highest MIDI note (inclusive) in the pitch pool. 50 nir_strength: 0.0–1.0. Scales how strongly the NIR rules 51 influence candidate scores. 0.0 = uniform; 1.0 = full boost. 52 chord_weight: 0.0–1.0. Additive multiplier bonus for candidates 53 whose pitch class belongs to the current chord tones. 54 rest_probability: 0.0–1.0. Probability of producing a rest 55 (returning ``None``) at any given step. 56 pitch_diversity: 0.0–1.0. Exponential penalty per recent 57 repetition of the same pitch. Lower values discourage 58 repetition more aggressively. 59 """ 60 61 self.key = key 62 self.mode = mode 63 self.low = low 64 self.high = high 65 self.nir_strength = nir_strength 66 self.chord_weight = chord_weight 67 self.rest_probability = rest_probability 68 self.pitch_diversity = pitch_diversity 69 70 key_pc = subsequence.chords.key_name_to_pc(key) 71 72 # Pitch pool: all scale tones within [low, high]. 73 self._pitch_pool: typing.List[int] = subsequence.intervals.scale_notes( 74 key, mode, low=low, high=high 75 ) 76 77 # Tonic pitch class for Rule C (closure). 78 self._tonic_pc: int = key_pc 79 80 # History of last N absolute MIDI pitches (capped at 4, same as HarmonicState). 81 self.history: typing.List[int] = [] 82 83 84 def choose_next ( 85 self, 86 chord_tones: typing.Optional[typing.List[int]], 87 rng: random.Random, 88 ) -> typing.Optional[int]: 89 90 """Score all pitch-pool candidates and return the chosen pitch, or None for a rest.""" 91 92 if self.rest_probability > 0.0 and rng.random() < self.rest_probability: 93 return None 94 95 if not self._pitch_pool: 96 return None 97 98 # Resolve chord tones to pitch classes for fast membership testing. 99 chord_tone_pcs: typing.Set[int] = ( 100 {t % 12 for t in chord_tones} if chord_tones else set() 101 ) 102 103 scores = [self._score_candidate(p, chord_tone_pcs) for p in self._pitch_pool] 104 105 # Weighted random choice: select using cumulative score as a probability weight. 106 total = sum(scores) 107 108 if total <= 0.0: 109 chosen = rng.choice(self._pitch_pool) 110 111 else: 112 r = rng.uniform(0.0, total) 113 cumulative = 0.0 114 chosen = self._pitch_pool[-1] 115 116 for pitch, score in zip(self._pitch_pool, scores): 117 cumulative += score 118 if r <= cumulative: 119 chosen = pitch 120 break 121 122 # Persist history for the next call (capped at 4 entries). 123 self.history.append(chosen) 124 if len(self.history) > 4: 125 self.history.pop(0) 126 127 return chosen 128 129 130 def _score_candidate ( 131 self, 132 candidate: int, 133 chord_tone_pcs: typing.Set[int], 134 ) -> float: 135 136 """Score one candidate pitch using NIR rules, chord weighting, range gravity, and pitch diversity.""" 137 138 score = 1.0 139 140 # --- NIR rules (require at least one history note for Realization) --- 141 if self.history: 142 last_note = self.history[-1] 143 144 target_diff = candidate - last_note 145 target_interval = abs(target_diff) 146 target_direction = 1 if target_diff > 0 else -1 if target_diff < 0 else 0 147 148 # Rules A & B require an Implication context (prev -> last -> candidate). 149 if len(self.history) >= 2: 150 prev_note = self.history[-2] 151 152 prev_diff = last_note - prev_note 153 prev_interval = abs(prev_diff) 154 prev_direction = 1 if prev_diff > 0 else -1 if prev_diff < 0 else 0 155 156 # Rule A: Reversal (gap fill) — after a large leap, expect direction change. 157 if prev_interval > 4: 158 if target_direction != prev_direction and target_direction != 0: 159 score += 0.5 160 161 if target_interval < 4: 162 score += 0.3 163 164 # Rule B: Process (continuation) — after a small step, expect more of the same. 165 elif 0 < prev_interval < 3: 166 if target_direction == prev_direction: 167 score += 0.4 168 169 if abs(target_interval - prev_interval) <= 1: 170 score += 0.2 171 172 # Rule C: Closure — the tonic is a cognitively stable landing point. 173 if candidate % 12 == self._tonic_pc: 174 score += 0.2 175 176 # Rule D: Proximity — smaller intervals are generally preferred. 177 if 0 < target_interval <= 3: 178 score += 0.3 179 180 # Scale the entire NIR boost by nir_strength, leaving the base at 1.0. 181 score = 1.0 + (score - 1.0) * self.nir_strength 182 183 # --- Chord tone boost --- 184 if candidate % 12 in chord_tone_pcs: 185 score *= 1.0 + self.chord_weight 186 187 # --- Range gravity: penalise notes far from the centre of [low, high] --- 188 centre = (self.low + self.high) / 2.0 189 half_range = max(1.0, (self.high - self.low) / 2.0) 190 distance_ratio = abs(candidate - centre) / half_range 191 score *= 1.0 - 0.3 * (distance_ratio ** 2) 192 193 # --- Pitch diversity: exponential penalty for recently-heard pitches --- 194 recent_occurrences = sum(1 for h in self.history if h == candidate) 195 score *= self.pitch_diversity ** recent_occurrences 196 197 return max(0.0, score)
Persistent melodic context that applies NIR scoring to single-note lines.
29 def __init__ ( 30 self, 31 key: str = "C", 32 mode: str = "ionian", 33 low: int = 48, 34 high: int = 72, 35 nir_strength: float = 0.5, 36 chord_weight: float = 0.4, 37 rest_probability: float = 0.0, 38 pitch_diversity: float = 0.6, 39 ) -> None: 40 41 """Initialise a melodic state for a given key, mode, and MIDI register. 42 43 Parameters: 44 key: Root note of the key (e.g. ``"C"``, ``"F#"``, ``"Bb"``). 45 mode: Scale mode name. Accepts any mode registered with 46 :func:`~subsequence.intervals.scale_pitch_classes` (e.g. 47 ``"ionian"``, ``"aeolian"``, ``"dorian"``). 48 low: Lowest MIDI note (inclusive) in the pitch pool. 49 high: Highest MIDI note (inclusive) in the pitch pool. 50 nir_strength: 0.0–1.0. Scales how strongly the NIR rules 51 influence candidate scores. 0.0 = uniform; 1.0 = full boost. 52 chord_weight: 0.0–1.0. Additive multiplier bonus for candidates 53 whose pitch class belongs to the current chord tones. 54 rest_probability: 0.0–1.0. Probability of producing a rest 55 (returning ``None``) at any given step. 56 pitch_diversity: 0.0–1.0. Exponential penalty per recent 57 repetition of the same pitch. Lower values discourage 58 repetition more aggressively. 59 """ 60 61 self.key = key 62 self.mode = mode 63 self.low = low 64 self.high = high 65 self.nir_strength = nir_strength 66 self.chord_weight = chord_weight 67 self.rest_probability = rest_probability 68 self.pitch_diversity = pitch_diversity 69 70 key_pc = subsequence.chords.key_name_to_pc(key) 71 72 # Pitch pool: all scale tones within [low, high]. 73 self._pitch_pool: typing.List[int] = subsequence.intervals.scale_notes( 74 key, mode, low=low, high=high 75 ) 76 77 # Tonic pitch class for Rule C (closure). 78 self._tonic_pc: int = key_pc 79 80 # History of last N absolute MIDI pitches (capped at 4, same as HarmonicState). 81 self.history: typing.List[int] = []
Initialise a melodic state for a given key, mode, and MIDI register.
Arguments:
- key: Root note of the key (e.g.
"C","F#","Bb"). - mode: Scale mode name. Accepts any mode registered with
~subsequence.intervals.scale_pitch_classes()(e.g."ionian","aeolian","dorian"). - low: Lowest MIDI note (inclusive) in the pitch pool.
- high: Highest MIDI note (inclusive) in the pitch pool.
- nir_strength: 0.0–1.0. Scales how strongly the NIR rules influence candidate scores. 0.0 = uniform; 1.0 = full boost.
- chord_weight: 0.0–1.0. Additive multiplier bonus for candidates whose pitch class belongs to the current chord tones.
- rest_probability: 0.0–1.0. Probability of producing a rest
(returning
None) at any given step. - pitch_diversity: 0.0–1.0. Exponential penalty per recent repetition of the same pitch. Lower values discourage repetition more aggressively.
84 def choose_next ( 85 self, 86 chord_tones: typing.Optional[typing.List[int]], 87 rng: random.Random, 88 ) -> typing.Optional[int]: 89 90 """Score all pitch-pool candidates and return the chosen pitch, or None for a rest.""" 91 92 if self.rest_probability > 0.0 and rng.random() < self.rest_probability: 93 return None 94 95 if not self._pitch_pool: 96 return None 97 98 # Resolve chord tones to pitch classes for fast membership testing. 99 chord_tone_pcs: typing.Set[int] = ( 100 {t % 12 for t in chord_tones} if chord_tones else set() 101 ) 102 103 scores = [self._score_candidate(p, chord_tone_pcs) for p in self._pitch_pool] 104 105 # Weighted random choice: select using cumulative score as a probability weight. 106 total = sum(scores) 107 108 if total <= 0.0: 109 chosen = rng.choice(self._pitch_pool) 110 111 else: 112 r = rng.uniform(0.0, total) 113 cumulative = 0.0 114 chosen = self._pitch_pool[-1] 115 116 for pitch, score in zip(self._pitch_pool, scores): 117 cumulative += score 118 if r <= cumulative: 119 chosen = pitch 120 break 121 122 # Persist history for the next call (capped at 4 entries). 123 self.history.append(chosen) 124 if len(self.history) > 4: 125 self.history.pop(0) 126 127 return chosen
Score all pitch-pool candidates and return the chosen pitch, or None for a rest.
41@dataclasses.dataclass 42class Tuning: 43 44 """A microtonal tuning system expressed as cent offsets from the unison. 45 46 The ``cents`` list contains the cent values for scale degrees 1 through N. 47 Degree 0 (the unison, 0.0 cents) is always implicit and not stored. 48 The last entry is typically 1200.0 cents (the octave) for octave-repeating 49 scales, but any period is supported. 50 51 Create a ``Tuning`` from a file or programmatically: 52 53 Tuning.from_scl("meanquar.scl") # Scala .scl file 54 Tuning.from_cents([100, 200, ..., 1200]) # explicit cents 55 Tuning.from_ratios([9/8, 5/4, ..., 2]) # frequency ratios 56 Tuning.equal(19) # 19-tone equal temperament 57 """ 58 59 cents: typing.List[float] 60 description: str = "" 61 62 @property 63 def size (self) -> int: 64 """Number of scale degrees per period (the .scl ``count`` line).""" 65 return len(self.cents) 66 67 @property 68 def period_cents (self) -> float: 69 """Cent span of one period (typically 1200.0 for octave-repeating scales).""" 70 return self.cents[-1] if self.cents else 1200.0 71 72 # ── Factory methods ─────────────────────────────────────────────────────── 73 74 @classmethod 75 def from_scl (cls, source: typing.Union[str, os.PathLike]) -> "Tuning": 76 """Parse a Scala .scl file. 77 78 ``source`` is a file path. Lines beginning with ``!`` are comments. 79 The first non-comment line is the description. The second is the 80 integer count of pitch values. Each subsequent line is a pitch: 81 82 - Contains ``.`` → cents (float). 83 - Contains ``/`` or is a bare integer → ratio; converted to cents via 84 ``1200 × log₂(ratio)``. 85 86 Raises ``ValueError`` for malformed files. 87 """ 88 with open(source, "r", encoding="utf-8") as fh: 89 text = fh.read() 90 return cls._parse_scl_text(text) 91 92 @classmethod 93 def from_scl_string (cls, text: str) -> "Tuning": 94 """Parse a Scala .scl file from a string (useful for testing).""" 95 return cls._parse_scl_text(text) 96 97 @classmethod 98 def _parse_scl_text (cls, text: str) -> "Tuning": 99 lines = [line.rstrip() for line in text.splitlines()] 100 non_comment: typing.List[str] = [l for l in lines if not l.lstrip().startswith("!")] 101 102 if len(non_comment) < 2: 103 raise ValueError("Malformed .scl: need description + count lines") 104 105 description = non_comment[0].strip() 106 107 try: 108 count = int(non_comment[1].strip()) 109 except ValueError: 110 raise ValueError(f"Malformed .scl: expected integer count, got {non_comment[1]!r}") 111 112 pitch_lines = non_comment[2:2 + count] 113 114 if len(pitch_lines) < count: 115 raise ValueError( 116 f"Malformed .scl: expected {count} pitch values, got {len(pitch_lines)}" 117 ) 118 119 cents_list: typing.List[float] = [] 120 for raw in pitch_lines: 121 # Text after the pitch value is ignored (Scala spec) 122 token = raw.split()[0] if raw.split() else "" 123 cents_list.append(cls._parse_pitch_token(token)) 124 125 return cls(cents=cents_list, description=description) 126 127 @staticmethod 128 def _parse_pitch_token (token: str) -> float: 129 """Convert a single .scl pitch token to cents.""" 130 if not token: 131 raise ValueError("Empty pitch token in .scl file") 132 if "." in token: 133 # Cents value 134 return float(token) 135 if "/" in token: 136 # Ratio like 3/2 137 num_str, den_str = token.split("/", 1) 138 ratio = int(num_str) / int(den_str) 139 else: 140 # Bare integer like 2 (interpreted as 2/1) 141 ratio = float(token) 142 if ratio <= 0: 143 raise ValueError(f"Non-positive ratio in .scl: {token!r}") 144 return 1200.0 * math.log2(ratio) 145 146 @classmethod 147 def from_cents (cls, cents: typing.List[float], description: str = "") -> "Tuning": 148 """Construct a tuning from a list of cent values for degrees 1..N. 149 150 The implicit degree 0 (unison, 0.0 cents) is not included in ``cents``. 151 The last value is typically 1200.0 for an octave-repeating scale. 152 """ 153 return cls(cents=list(cents), description=description) 154 155 @classmethod 156 def from_ratios (cls, ratios: typing.List[float], description: str = "") -> "Tuning": 157 """Construct a tuning from frequency ratios relative to 1/1. 158 159 Each ratio is converted to cents via ``1200 × log₂(ratio)``. 160 Pass ``2`` or ``2.0`` for the octave (1200 cents). 161 """ 162 cents = [1200.0 * math.log2(r) for r in ratios] 163 return cls(cents=cents, description=description) 164 165 @classmethod 166 def equal (cls, divisions: int = 12, period: float = 1200.0) -> "Tuning": 167 """Construct an equal-tempered tuning with ``divisions`` equal steps per period. 168 169 ``Tuning.equal(12)`` is standard 12-TET (no pitch bend needed). 170 ``Tuning.equal(19)`` gives 19-tone equal temperament. 171 """ 172 step = period / divisions 173 cents = [step * i for i in range(1, divisions + 1)] 174 return cls( 175 cents=cents, 176 description=f"{divisions}-tone equal temperament", 177 ) 178 179 # ── Core calculation ────────────────────────────────────────────────────── 180 181 def pitch_bend_for_note ( 182 self, 183 midi_note: int, 184 reference_note: int = 60, 185 bend_range: float = 2.0, 186 ) -> typing.Tuple[int, float]: 187 """Return ``(nearest_12tet_note, bend_normalized)`` for a MIDI note number. 188 189 The MIDI note number is interpreted as a scale degree relative to 190 ``reference_note`` (default 60 = C4, degree 0 of the scale). The 191 tuning's cent table determines the exact frequency, and the nearest 192 12-TET MIDI note plus a fractional pitch bend corrects the remainder. 193 194 Parameters: 195 midi_note: The MIDI note to tune (0–127). 196 reference_note: MIDI note number that maps to degree 0 of the scale. 197 bend_range: Pitch wheel range in semitones (must match the synth's 198 pitch-bend range setting). Default ±2 semitones. 199 200 Returns: 201 A tuple ``(nearest_note, bend_normalized)`` where ``nearest_note`` 202 is the integer MIDI note to send and ``bend_normalized`` is the 203 normalised pitch bend value (-1.0 to +1.0). 204 """ 205 if self.size == 0: 206 return midi_note, 0.0 207 208 steps_from_root = midi_note - reference_note 209 degree = steps_from_root % self.size 210 octave = steps_from_root // self.size 211 212 # Cent value for this degree (degree 0 = 0.0, degree k = cents[k-1]) 213 degree_cents = 0.0 if degree == 0 else self.cents[degree - 1] 214 215 # Total cents from the root 216 total_cents = octave * self.period_cents + degree_cents 217 218 # Equivalent continuous 12-TET note number (100 cents per semitone) 219 continuous = reference_note + total_cents / 100.0 220 221 nearest = int(round(continuous)) 222 nearest = max(0, min(127, nearest)) 223 224 offset_semitones = continuous - nearest # signed, in semitones 225 226 if bend_range <= 0: 227 bend_normalized = 0.0 228 else: 229 bend_normalized = max(-1.0, min(1.0, offset_semitones / bend_range)) 230 231 return nearest, bend_normalized
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
62 @property 63 def size (self) -> int: 64 """Number of scale degrees per period (the .scl ``count`` line).""" 65 return len(self.cents)
Number of scale degrees per period (the .scl count line).
67 @property 68 def period_cents (self) -> float: 69 """Cent span of one period (typically 1200.0 for octave-repeating scales).""" 70 return self.cents[-1] if self.cents else 1200.0
Cent span of one period (typically 1200.0 for octave-repeating scales).
74 @classmethod 75 def from_scl (cls, source: typing.Union[str, os.PathLike]) -> "Tuning": 76 """Parse a Scala .scl file. 77 78 ``source`` is a file path. Lines beginning with ``!`` are comments. 79 The first non-comment line is the description. The second is the 80 integer count of pitch values. Each subsequent line is a pitch: 81 82 - Contains ``.`` → cents (float). 83 - Contains ``/`` or is a bare integer → ratio; converted to cents via 84 ``1200 × log₂(ratio)``. 85 86 Raises ``ValueError`` for malformed files. 87 """ 88 with open(source, "r", encoding="utf-8") as fh: 89 text = fh.read() 90 return cls._parse_scl_text(text)
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 via1200 × log₂(ratio).
Raises ValueError for malformed files.
92 @classmethod 93 def from_scl_string (cls, text: str) -> "Tuning": 94 """Parse a Scala .scl file from a string (useful for testing).""" 95 return cls._parse_scl_text(text)
Parse a Scala .scl file from a string (useful for testing).
146 @classmethod 147 def from_cents (cls, cents: typing.List[float], description: str = "") -> "Tuning": 148 """Construct a tuning from a list of cent values for degrees 1..N. 149 150 The implicit degree 0 (unison, 0.0 cents) is not included in ``cents``. 151 The last value is typically 1200.0 for an octave-repeating scale. 152 """ 153 return cls(cents=list(cents), description=description)
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.
155 @classmethod 156 def from_ratios (cls, ratios: typing.List[float], description: str = "") -> "Tuning": 157 """Construct a tuning from frequency ratios relative to 1/1. 158 159 Each ratio is converted to cents via ``1200 × log₂(ratio)``. 160 Pass ``2`` or ``2.0`` for the octave (1200 cents). 161 """ 162 cents = [1200.0 * math.log2(r) for r in ratios] 163 return cls(cents=cents, description=description)
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).
165 @classmethod 166 def equal (cls, divisions: int = 12, period: float = 1200.0) -> "Tuning": 167 """Construct an equal-tempered tuning with ``divisions`` equal steps per period. 168 169 ``Tuning.equal(12)`` is standard 12-TET (no pitch bend needed). 170 ``Tuning.equal(19)`` gives 19-tone equal temperament. 171 """ 172 step = period / divisions 173 cents = [step * i for i in range(1, divisions + 1)] 174 return cls( 175 cents=cents, 176 description=f"{divisions}-tone equal temperament", 177 )
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.
181 def pitch_bend_for_note ( 182 self, 183 midi_note: int, 184 reference_note: int = 60, 185 bend_range: float = 2.0, 186 ) -> typing.Tuple[int, float]: 187 """Return ``(nearest_12tet_note, bend_normalized)`` for a MIDI note number. 188 189 The MIDI note number is interpreted as a scale degree relative to 190 ``reference_note`` (default 60 = C4, degree 0 of the scale). The 191 tuning's cent table determines the exact frequency, and the nearest 192 12-TET MIDI note plus a fractional pitch bend corrects the remainder. 193 194 Parameters: 195 midi_note: The MIDI note to tune (0–127). 196 reference_note: MIDI note number that maps to degree 0 of the scale. 197 bend_range: Pitch wheel range in semitones (must match the synth's 198 pitch-bend range setting). Default ±2 semitones. 199 200 Returns: 201 A tuple ``(nearest_note, bend_normalized)`` where ``nearest_note`` 202 is the integer MIDI note to send and ``bend_normalized`` is the 203 normalised pitch bend value (-1.0 to +1.0). 204 """ 205 if self.size == 0: 206 return midi_note, 0.0 207 208 steps_from_root = midi_note - reference_note 209 degree = steps_from_root % self.size 210 octave = steps_from_root // self.size 211 212 # Cent value for this degree (degree 0 = 0.0, degree k = cents[k-1]) 213 degree_cents = 0.0 if degree == 0 else self.cents[degree - 1] 214 215 # Total cents from the root 216 total_cents = octave * self.period_cents + degree_cents 217 218 # Equivalent continuous 12-TET note number (100 cents per semitone) 219 continuous = reference_note + total_cents / 100.0 220 221 nearest = int(round(continuous)) 222 nearest = max(0, min(127, nearest)) 223 224 offset_semitones = continuous - nearest # signed, in semitones 225 226 if bend_range <= 0: 227 bend_normalized = 0.0 228 else: 229 bend_normalized = max(-1.0, min(1.0, offset_semitones / bend_range)) 230 231 return nearest, bend_normalized
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.
Arguments:
- 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)wherenearest_noteis the integer MIDI note to send andbend_normalizedis the normalised pitch bend value (-1.0 to +1.0).
358def register_scale ( 359 name: str, 360 intervals: typing.List[int], 361 qualities: typing.Optional[typing.List[str]] = None 362) -> None: 363 364 """ 365 Register a custom scale for use with ``p.quantize()`` and 366 ``scale_pitch_classes()``. 367 368 Parameters: 369 name: Scale name (used in ``p.quantize(key, name)``). 370 intervals: Semitone offsets from the root (e.g. ``[0, 2, 3, 7, 8]`` 371 for Hirajōshi). Must start with 0 and contain values 0–11. 372 qualities: Optional chord quality per scale degree (e.g. 373 ``["minor", "major", "minor", "major", "diminished"]``). 374 Required only if you want to use the scale with 375 ``diatonic_chords()`` or ``diatonic_chord_sequence()``. 376 377 Example:: 378 379 import subsequence 380 381 subsequence.register_scale("raga_bhairav", [0, 1, 4, 5, 7, 8, 11]) 382 383 @comp.pattern(channel=0, length=4) 384 def melody (p): 385 p.note(60, beat=0) 386 p.quantize("C", "raga_bhairav") 387 """ 388 389 if not intervals or intervals[0] != 0: 390 raise ValueError("intervals must start with 0") 391 if any(i < 0 or i > 11 for i in intervals): 392 raise ValueError("intervals must contain values between 0 and 11") 393 if qualities is not None and len(qualities) != len(intervals): 394 raise ValueError( 395 f"qualities length ({len(qualities)}) must match " 396 f"intervals length ({len(intervals)})" 397 ) 398 399 INTERVAL_DEFINITIONS[name] = intervals 400 SCALE_MODE_MAP[name] = (name, qualities)
Register a custom scale for use with p.quantize() and
scale_pitch_classes().
Arguments:
- name: Scale name (used in
p.quantize(key, name)). - intervals: Semitone offsets from the root (e.g.
[0, 2, 3, 7, 8]for Hirajōshi). Must start with 0 and contain values 0–11. - qualities: Optional chord quality per scale degree (e.g.
["minor", "major", "minor", "major", "diminished"]). Required only if you want to use the scale withdiatonic_chords()ordiatonic_chord_sequence().
Example::
import subsequence
subsequence.register_scale("raga_bhairav", [0, 1, 4, 5, 7, 8, 11])
@comp.pattern(channel=0, length=4)
def melody (p):
p.note(60, beat=0)
p.quantize("C", "raga_bhairav")
214def scale_notes ( 215 key: str, 216 mode: str = "ionian", 217 low: int = 60, 218 high: int = 72, 219 count: typing.Optional[int] = None, 220) -> typing.List[int]: 221 222 """Return MIDI note numbers for a scale within a pitch range. 223 224 Parameters: 225 key: Scale root as a note name (``"C"``, ``"F#"``, ``"Bb"``, etc.). 226 This acts as a **pitch-class filter only** — it determines which 227 semitone positions (0–11) are valid members of the scale, but does 228 not affect which octave notes are drawn from. Notes are selected 229 starting from ``low`` upward; ``key`` controls *which* notes are 230 kept, not where the sequence starts. To guarantee the first 231 returned note is the root, ``low`` must be a MIDI number whose 232 pitch class matches ``key``. When starting from an arbitrary MIDI 233 number, derive the key name with 234 ``subsequence.chords.PC_TO_NOTE_NAME[root_pitch % 12]``. 235 mode: Scale mode name. Supports all keys of :data:`SCALE_MODE_MAP` 236 (e.g. ``"ionian"``, ``"dorian"``, ``"natural_minor"``, 237 ``"major_pentatonic"``). Use :func:`register_scale` for custom scales. 238 low: Lowest MIDI note (inclusive). When ``count`` is set, this is 239 the starting note from which the scale ascends. **If ``low`` is 240 not a member of the scale defined by ``key``, it is silently 241 skipped** and the first returned note will be the next in-scale 242 pitch above ``low``. 243 high: Highest MIDI note (inclusive). Ignored when ``count`` is set. 244 count: Exact number of notes to return. Notes ascend from ``low`` 245 through successive scale degrees, cycling into higher octaves 246 as needed. When ``None`` (default), all scale tones between 247 ``low`` and ``high`` are returned. 248 249 Returns: 250 Sorted list of MIDI note numbers. 251 252 Examples: 253 ```python 254 import subsequence 255 import subsequence.constants.midi_notes as notes 256 257 # C major: all tones from middle C to C5 258 subsequence.scale_notes("C", "ionian", low=notes.C4, high=notes.C5) 259 # → [60, 62, 64, 65, 67, 69, 71, 72] 260 261 # E natural minor (aeolian) across one octave 262 subsequence.scale_notes("E", "aeolian", low=notes.E2, high=notes.E3) 263 # → [40, 42, 43, 45, 47, 48, 50, 52] 264 265 # 15 notes of A minor pentatonic ascending from A3 266 subsequence.scale_notes("A", "minor_pentatonic", low=notes.A3, count=15) 267 # → [57, 60, 62, 64, 67, 69, 72, 74, 76, 79, 81, 84, 86, 88, 91] 268 269 # Misalignment: key="E" but low=C4 — first note is C, not E 270 subsequence.scale_notes("E", "minor", low=60, count=4) 271 # → [60, 62, 64, 67] (C D E G — all in E natural minor, but starts on C) 272 273 # Fix: derive key name from root_pitch so low is always in the scale 274 root_pitch = 64 # E4 275 key = subsequence.chords.PC_TO_NOTE_NAME[root_pitch % 12] # → "E" 276 subsequence.scale_notes(key, "minor", low=root_pitch, count=4) 277 # → [64, 66, 67, 69] (E F# G A — starts on the root) 278 ``` 279 """ 280 281 key_pc = subsequence.chords.key_name_to_pc(key) 282 pcs = set(scale_pitch_classes(key_pc, mode)) 283 284 if count is not None: 285 if not pcs: 286 return [] 287 result: typing.List[int] = [] 288 pitch = low 289 while len(result) < count and pitch <= 127: 290 if pitch % 12 in pcs: 291 result.append(pitch) 292 pitch += 1 293 return result 294 295 return [p for p in range(low, high + 1) if p % 12 in pcs]
Return MIDI note numbers for a scale within a pitch range.
Arguments:
- key: Scale root as a note name (
"C","F#","Bb", etc.). This acts as a pitch-class filter only — it determines which semitone positions (0–11) are valid members of the scale, but does not affect which octave notes are drawn from. Notes are selected starting fromlowupward;keycontrols which notes are kept, not where the sequence starts. To guarantee the first returned note is the root,lowmust be a MIDI number whose pitch class matcheskey. When starting from an arbitrary MIDI number, derive the key name withsubsequence.chords.PC_TO_NOTE_NAME[root_pitch % 12]. - mode: Scale mode name. Supports all keys of
SCALE_MODE_MAP(e.g."ionian","dorian","natural_minor","major_pentatonic"). Useregister_scale()for custom scales. - low: Lowest MIDI note (inclusive). When
countis set, this is the starting note from which the scale ascends. Iflowis not a member of the scale defined bykey, it is silently skipped and the first returned note will be the next in-scale pitch abovelow. - high: Highest MIDI note (inclusive). Ignored when
countis set. - count: Exact number of notes to return. Notes ascend from
lowthrough successive scale degrees, cycling into higher octaves as needed. WhenNone(default), all scale tones betweenlowandhighare returned.
Returns:
Sorted list of MIDI note numbers.
Examples:
import subsequence import subsequence.constants.midi_notes as notes # C major: all tones from middle C to C5 subsequence.scale_notes("C", "ionian", low=notes.C4, high=notes.C5) # → [60, 62, 64, 65, 67, 69, 71, 72] # E natural minor (aeolian) across one octave subsequence.scale_notes("E", "aeolian", low=notes.E2, high=notes.E3) # → [40, 42, 43, 45, 47, 48, 50, 52] # 15 notes of A minor pentatonic ascending from A3 subsequence.scale_notes("A", "minor_pentatonic", low=notes.A3, count=15) # → [57, 60, 62, 64, 67, 69, 72, 74, 76, 79, 81, 84, 86, 88, 91] # Misalignment: key="E" but low=C4 — first note is C, not E subsequence.scale_notes("E", "minor", low=60, count=4) # → [60, 62, 64, 67] (C D E G — all in E natural minor, but starts on C) # Fix: derive key name from root_pitch so low is always in the scale root_pitch = 64 # E4 key = subsequence.chords.PC_TO_NOTE_NAME[root_pitch % 12] # → "E" subsequence.scale_notes(key, "minor", low=root_pitch, count=4) # → [64, 66, 67, 69] (E F# G A — starts on the root)
111def bank_select (bank: int) -> typing.Tuple[int, int]: 112 113 """ 114 Convert a 14-bit MIDI bank number to (MSB, LSB) for use with 115 ``p.program_change()``. 116 117 MIDI bank select uses two control-change messages: CC 0 (Bank MSB) and 118 CC 32 (Bank LSB). Together they encode a 14-bit bank number in the 119 range 0–16,383: 120 121 MSB = bank // 128 (upper 7 bits, sent on CC 0) 122 LSB = bank % 128 (lower 7 bits, sent on CC 32) 123 124 Args: 125 bank: Integer bank number, 0–16,383. Values outside this range are 126 clamped. 127 128 Returns: 129 ``(msb, lsb)`` tuple, each value in 0–127. 130 131 Example: 132 ```python 133 msb, lsb = subsequence.bank_select(128) # → (1, 0) 134 p.program_change(48, bank_msb=msb, bank_lsb=lsb) 135 ``` 136 """ 137 138 bank = max(0, min(16383, bank)) 139 return bank >> 7, bank & 0x7F
Convert a 14-bit MIDI bank number to (MSB, LSB) for use with
p.program_change().
MIDI bank select uses two control-change messages: CC 0 (Bank MSB) and CC 32 (Bank LSB). Together they encode a 14-bit bank number in the range 0–16,383:
MSB = bank // 128 (upper 7 bits, sent on CC 0)
LSB = bank % 128 (lower 7 bits, sent on CC 32)
Arguments:
- bank: Integer bank number, 0–16,383. Values outside this range are clamped.
Returns:
(msb, lsb)tuple, each value in 0–127.
Example:
msb, lsb = subsequence.bank_select(128) # → (1, 0) p.program_change(48, bank_msb=msb, bank_lsb=lsb)