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