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.
- Cognitive harmony engine. Chord progressions evolve via weighted transition graphs with adjustable gravity and Narmour-based melodic inertia. Eleven built-in palettes, automatic voice leading, and frozen progressions to lock some sections while others evolve freely.
- 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, 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. - 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). 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=9, length=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, 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- **Cognitive harmony engine.** Chord progressions evolve via weighted 29 transition graphs with adjustable gravity and Narmour-based melodic 30 inertia. Eleven built-in palettes, automatic voice leading, and frozen 31 progressions to lock some sections while others evolve freely. 32- **Sub-microsecond clock.** Hybrid sleep+spin timing achieves typical 33 pulse jitter of < 5 us on Linux, with zero long-term drift. 34- **Turn anything into music.** ``composition.schedule()`` runs any 35 Python function on a beat cycle - APIs, sensors, files. Anything 36 Python can reach becomes a musical parameter. 37- **Pure MIDI, zero sound engine.** No audio synthesis, no heavyweight 38 dependencies. Route to hardware synths, drum machines, Eurorack, or 39 software instruments. 40 41Composition tools: 42 43- **Rhythm and feel.** Euclidean and Bresenham generators, multi-voice 44 weighted Bresenham distribution (``bresenham_poly()``), ghost note 45 layers (``ghost_fill()``), position-aware note removal (``thin()`` - 46 the musical inverse of ``ghost_fill``), evolving cellular-automaton 47 rhythms (``cellular_1d()``, ``cellular_2d()``), smooth Perlin noise (``perlin_1d()``, 48 ``perlin_2d()``, ``perlin_1d_sequence()``, ``perlin_2d_grid()``), 49 deterministic chaos sequences (``logistic_map()``), pink 1/f noise 50 (``pink_noise()``), L-system string rewriting (``p.lsystem()``), 51 Markov-chain generation (``p.markov()``), aperiodic binary rhythms 52 (``p.thue_morse()``), golden-ratio beat placement (``p.fibonacci()``), 53 Gray-Scott reaction-diffusion patterns (``p.reaction_diffusion()``), 54 Lorenz strange-attractor generation (``p.lorenz()``), exhaustive 55 pitch-subsequence melodies (``p.de_bruijn()``), step-wise melodies 56 with guaranteed pitch diversity (``p.self_avoiding_walk()``), drones 57 and explicit note on/off events (``p.drone()``, ``p.drone_off()``, 58 ``p.silence()``), 59 groove templates (``Groove.swing()``, ``Groove.from_agr()``), swing via 60 ``p.swing()`` (a shortcut for ``Groove.swing()``), randomize, 61 velocity shaping, dropout, per-step probability, and polyrhythms 62 via independent pattern lengths. 63- **Melody generation.** ``p.melody()`` with ``MelodicState`` applies 64 the Narmour Implication-Realization model to single-note lines: 65 continuation after small steps, reversal after large leaps, chord-tone 66 weighting, range gravity, and pitch-diversity penalty. History persists 67 across bar rebuilds for natural phrase continuity. 68- **Expression.** CC messages/ramps, pitch bend, note-correlated 69 bend/portamento/slide, program changes, SysEx, and OSC output - all 70 from within patterns. 71- **Form and structure.** Musical form as a weighted graph, ordered list, 72 or generator. Patterns read ``p.section`` to adapt. Conductor signals 73 (LFOs, ramps) shape intensity over time. 74- **Mini-notation.** ``p.seq("x x [x x] x", pitch="kick")`` - concise 75 string syntax for rhythms, subdivisions, and per-step probability. 76- **Scales and quantization.** ``p.quantize()`` snaps notes to any 77 scale. ``scale_notes()`` generates a list of MIDI note numbers from 78 a key, mode, and range or note count - useful for arpeggios, Markov 79 chains, and melodic walks. Built-in western and non-western modes, 80 plus ``register_scale()`` for your own. 81- **Randomness tools.** Weighted choice, no-repeat shuffle, random 82 walk, probability gates. Deterministic seeding (``seed=42``) makes 83 every decision repeatable. 84- **Pattern transforms.** Legato, staccato, reverse, double/half-time, 85 shift, transpose, invert, randomize, and conditional ``p.every()``. 86 87Integration: 88 89- **MIDI clock.** Master (``clock_output()``) or follower 90 (``clock_follow=True``). Sync to a DAW or drive hardware. 91- **Hardware control.** CC input mapping from knobs/faders to 92 ``composition.data``; patterns read and write the same dict via 93 ``p.data`` for both external data access and cross-pattern 94 communication. OSC for bidirectional communication with mixers, 95 lighting, visuals. 96- **Live coding.** Hot-swap patterns, change tempo, mute/unmute, and 97 tweak parameters during playback via a built-in TCP eval server. 98- **Hotkeys.** Single keystrokes to jump sections, toggle mutes, or 99 fire any action - with optional bar-boundary quantization. 100- **Real-time pattern triggering.** ``composition.trigger()`` generates 101 one-shot patterns in response to sensors, OSC, or any event. 102- **Terminal display.** Live status line (BPM, bar, section, chord). 103 Add ``grid=True`` for an ASCII pattern grid showing velocity and 104 sustain - makes legato and staccato visually distinct at a glance. 105 Add ``grid_scale=2`` to zoom in horizontally, revealing swing and 106 groove micro-timing. 107- **Web UI Dashboard (Beta).** Enable with ``composition.web_ui()`` to 108 broadcast live composition metadata and visualize piano-roll pattern 109 grids in a reactive HTTP/WebSocket browser dashboard. 110- **Ableton Link.** Industry-standard wireless tempo/phase sync 111 (``comp.link()``; requires ``pip install subsequence[link]``). 112 Any Link-enabled app on the same LAN — Ableton Live, iOS synths, 113 other Subsequence instances — stays in time automatically. 114- **Recording.** Record to standard MIDI file. Render to file without 115 waiting for real-time playback. 116 117Minimal example: 118 119 ```python 120 import subsequence 121 import subsequence.constants.instruments.gm_drums as gm_drums 122 123 comp = subsequence.Composition(bpm=120) 124 125 @comp.pattern(channel=9, length=4, drum_note_map=gm_drums.GM_DRUM_MAP) 126 def drums (p): 127 (p.hit_steps("kick_1", [0, 4, 8, 12], velocity=100) 128 .hit_steps("snare_1", [4, 12], velocity=90) 129 .hit_steps("hi_hat_closed", range(16), velocity=70)) 130 131 comp.play() 132 ``` 133 134Community and Feedback: 135 136- **Discussions:** Chat and ask questions at https://github.com/simonholliday/subsequence/discussions 137- **Issues:** Report bugs and request features at https://github.com/simonholliday/subsequence/issues 138 139Package-level exports: ``Composition``, ``Groove``, ``MelodicState``, ``register_scale``, ``scale_notes``, ``bank_select``. 140""" 141 142import subsequence.composition 143import subsequence.groove 144import subsequence.intervals 145import subsequence.melodic_state 146import subsequence.midi_utils 147 148 149Composition = subsequence.composition.Composition 150Groove = subsequence.groove.Groove 151MelodicState = subsequence.melodic_state.MelodicState 152register_scale = subsequence.intervals.register_scale 153scale_notes = subsequence.intervals.scale_notes 154bank_select = subsequence.midi_utils.bank_select
551class Composition: 552 553 """ 554 The top-level controller for a musical piece. 555 556 The `Composition` object manages the global clock (Sequencer), the harmonic 557 progression (HarmonicState), the song structure (subsequence.form_state.FormState), and all MIDI patterns. 558 It serves as the main entry point for defining your music. 559 560 Typical workflow: 561 1. Initialize `Composition` with BPM and Key. 562 2. Define harmony and form (optional). 563 3. Register patterns using the `@composition.pattern` decorator. 564 4. Call `composition.play()` to start the music. 565 """ 566 567 def __init__ ( 568 self, 569 output_device: typing.Optional[str] = None, 570 bpm: float = 120, 571 time_signature: typing.Tuple[int, int] = (4, 4), 572 key: typing.Optional[str] = None, 573 seed: typing.Optional[int] = None, 574 record: bool = False, 575 record_filename: typing.Optional[str] = None, 576 zero_indexed_channels: bool = True 577 ) -> None: 578 579 """ 580 Initialize a new composition. 581 582 Parameters: 583 output_device: The name of the MIDI output port to use. If `None`, 584 Subsequence will attempt to find a device, prompting if necessary. 585 bpm: Initial tempo in beats per minute (default 120). 586 key: The root key of the piece (e.g., "C", "F#", "Bb"). 587 Required if you plan to use `harmony()`. 588 seed: An optional integer for deterministic randomness. When set, 589 every random decision (chord choices, drum probability, etc.) 590 will be identical on every run. 591 record: When True, record all MIDI events to a file. 592 record_filename: Optional filename for the recording (defaults to timestamp). 593 zero_indexed_channels: When False, MIDI channels use 594 1-based numbering (1-16) matching instrument labelling. 595 Channel 10 is drums, the way musicians and hardware panels 596 show it. When True (default), channels use 0-based numbering (0-15) 597 matching the raw MIDI protocol. 598 599 Example: 600 ```python 601 comp = subsequence.Composition(bpm=128, key="Eb", seed=123) 602 ``` 603 """ 604 605 self.output_device = output_device 606 self.bpm = bpm 607 self.time_signature = time_signature 608 self.key = key 609 self._seed: typing.Optional[int] = seed 610 self._zero_indexed_channels: bool = zero_indexed_channels 611 612 self._sequencer = subsequence.sequencer.Sequencer( 613 output_device_name = output_device, 614 initial_bpm = bpm, 615 time_signature = time_signature, 616 record = record, 617 record_filename = record_filename 618 ) 619 620 self._harmonic_state: typing.Optional[subsequence.harmonic_state.HarmonicState] = None 621 self._harmony_cycle_beats: typing.Optional[int] = None 622 self._harmony_reschedule_lookahead: float = 1 623 self._section_progressions: typing.Dict[str, Progression] = {} 624 self._pending_patterns: typing.List[_PendingPattern] = [] 625 self._pending_scheduled: typing.List[_PendingScheduled] = [] 626 self._form_state: typing.Optional[subsequence.form_state.FormState] = None 627 self._builder_bar: int = 0 628 self._display: typing.Optional[subsequence.display.Display] = None 629 self._live_server: typing.Optional[subsequence.live_server.LiveServer] = None 630 self._is_live: bool = False 631 self._running_patterns: typing.Dict[str, typing.Any] = {} 632 self._input_device: typing.Optional[str] = None 633 self._clock_follow: bool = False 634 self._clock_output: bool = False 635 self._cc_mappings: typing.List[typing.Dict[str, typing.Any]] = [] 636 self.data: typing.Dict[str, typing.Any] = {} 637 self._osc_server: typing.Optional[subsequence.osc.OscServer] = None 638 self.conductor = subsequence.conductor.Conductor() 639 self._web_ui_enabled: bool = False 640 self._web_ui_server: typing.Optional[subsequence.web_ui.WebUI] = None 641 self._link_quantum: typing.Optional[float] = None 642 643 # Hotkey state — populated by hotkeys() and hotkey(). 644 self._hotkeys_enabled: bool = False 645 self._hotkey_bindings: typing.Dict[str, HotkeyBinding] = {} 646 self._pending_hotkey_actions: typing.List[_PendingHotkeyAction] = [] 647 self._keystroke_listener: typing.Optional[subsequence.keystroke.KeystrokeListener] = None 648 649 def _resolve_channel (self, channel: int) -> int: 650 651 """ 652 Convert a user-supplied MIDI channel to the 0-indexed value used internally. 653 654 When ``zero_indexed_channels`` is True, the channel is validated as 655 0-15 and returned unchanged. When False (1-indexed), the channel is 656 validated as 1-16 and decremented by one. 657 """ 658 659 if self._zero_indexed_channels: 660 if not 0 <= channel <= 15: 661 raise ValueError(f"MIDI channel must be 0-15 (zero_indexed_channels=True), got {channel}") 662 return channel 663 else: 664 if not 1 <= channel <= 16: 665 raise ValueError(f"MIDI channel must be 1-16, got {channel}") 666 return channel - 1 667 668 @property 669 def harmonic_state (self) -> typing.Optional[subsequence.harmonic_state.HarmonicState]: 670 """The active ``HarmonicState``, or ``None`` if ``harmony()`` has not been called.""" 671 return self._harmonic_state 672 673 @property 674 def form_state (self) -> typing.Optional["subsequence.form_state.FormState"]: 675 """The active ``subsequence.form_state.FormState``, or ``None`` if ``form()`` has not been called.""" 676 return self._form_state 677 678 @property 679 def sequencer (self) -> subsequence.sequencer.Sequencer: 680 """The underlying ``Sequencer`` instance.""" 681 return self._sequencer 682 683 @property 684 def running_patterns (self) -> typing.Dict[str, typing.Any]: 685 """The currently active patterns, keyed by name.""" 686 return self._running_patterns 687 688 @property 689 def builder_bar (self) -> int: 690 """Current bar index used by pattern builders.""" 691 return self._builder_bar 692 693 def _require_harmonic_state (self) -> subsequence.harmonic_state.HarmonicState: 694 """Return the active HarmonicState, raising ValueError if none is configured.""" 695 if self._harmonic_state is None: 696 raise ValueError( 697 "harmony() must be called before this action — " 698 "no harmonic state has been configured." 699 ) 700 return self._harmonic_state 701 702 def harmony ( 703 self, 704 style: typing.Union[str, subsequence.chord_graphs.ChordGraph] = "functional_major", 705 cycle_beats: int = 4, 706 dominant_7th: bool = True, 707 gravity: float = 1.0, 708 nir_strength: float = 0.5, 709 minor_turnaround_weight: float = 0.0, 710 root_diversity: float = subsequence.harmonic_state.DEFAULT_ROOT_DIVERSITY, 711 reschedule_lookahead: float = 1 712 ) -> None: 713 714 """ 715 Configure the harmonic logic and chord change intervals. 716 717 Subsequence uses a weighted transition graph to choose the next chord. 718 You can influence these choices using 'gravity' (favoring the tonic) and 719 'NIR strength' (melodic inertia based on Narmour's model). 720 721 Parameters: 722 style: The harmonic style to use. Built-in: "functional_major" 723 (alias "diatonic_major"), "turnaround", "aeolian_minor", 724 "phrygian_minor", "lydian_major", "dorian_minor", 725 "chromatic_mediant", "suspended", "mixolydian", "whole_tone", 726 "diminished". See README for full descriptions. 727 cycle_beats: How many beats each chord lasts (default 4). 728 dominant_7th: Whether to include V7 chords (default True). 729 gravity: Key gravity (0.0 to 1.0). High values stay closer to the root chord. 730 nir_strength: Melodic inertia (0.0 to 1.0). Influences chord movement 731 expectations. 732 minor_turnaround_weight: For "turnaround" style, influences major vs minor feel. 733 root_diversity: Root-repetition damping (0.0 to 1.0). Each recent 734 chord sharing a candidate's root reduces the weight to 40% at 735 the default (0.4). Set to 1.0 to disable. 736 reschedule_lookahead: How many beats in advance to calculate the 737 next chord. 738 739 Example: 740 ```python 741 # A moody minor progression that changes every 8 beats 742 comp.harmony(style="aeolian_minor", cycle_beats=8, gravity=0.4) 743 ``` 744 """ 745 746 if self.key is None: 747 raise ValueError("Cannot configure harmony without a key - set key in the Composition constructor") 748 749 preserved_history: typing.List[subsequence.chords.Chord] = [] 750 preserved_current: typing.Optional[subsequence.chords.Chord] = None 751 752 if self._harmonic_state is not None: 753 preserved_history = self._harmonic_state.history.copy() 754 preserved_current = self._harmonic_state.current_chord 755 756 self._harmonic_state = subsequence.harmonic_state.HarmonicState( 757 key_name = self.key, 758 graph_style = style, 759 include_dominant_7th = dominant_7th, 760 key_gravity_blend = gravity, 761 nir_strength = nir_strength, 762 minor_turnaround_weight = minor_turnaround_weight, 763 root_diversity = root_diversity 764 ) 765 766 if preserved_history: 767 self._harmonic_state.history = preserved_history 768 if preserved_current is not None and self._harmonic_state.graph.get_transitions(preserved_current): 769 self._harmonic_state.current_chord = preserved_current 770 771 self._harmony_cycle_beats = cycle_beats 772 self._harmony_reschedule_lookahead = reschedule_lookahead 773 774 def freeze (self, bars: int) -> "Progression": 775 776 """Capture a chord progression from the live harmony engine. 777 778 Runs the harmony engine forward by *bars* chord changes, records each 779 chord, and returns it as a :class:`Progression` that can be bound to a 780 form section with :meth:`section_chords`. 781 782 The engine state **advances** — successive ``freeze()`` calls produce a 783 continuing compositional journey so section progressions feel like parts 784 of a whole rather than isolated islands. 785 786 Parameters: 787 bars: Number of chords to capture (one per harmony cycle). 788 789 Returns: 790 A :class:`Progression` with the captured chords and trailing 791 history for NIR continuity. 792 793 Raises: 794 ValueError: If :meth:`harmony` has not been called first. 795 796 Example:: 797 798 composition.harmony(style="functional_major", cycle_beats=4) 799 verse = composition.freeze(8) # 8 chords, engine advances 800 chorus = composition.freeze(4) # next 4 chords, continuing on 801 composition.section_chords("verse", verse) 802 composition.section_chords("chorus", chorus) 803 """ 804 805 hs = self._require_harmonic_state() 806 807 if bars < 1: 808 raise ValueError("bars must be at least 1") 809 collected: typing.List[subsequence.chords.Chord] = [hs.current_chord] 810 811 for _ in range(bars - 1): 812 hs.step() 813 collected.append(hs.current_chord) 814 815 # Advance past the last captured chord so the next freeze() call or 816 # live playback does not duplicate it. 817 hs.step() 818 819 return Progression( 820 chords = tuple(collected), 821 trailing_history = tuple(hs.history), 822 ) 823 824 def section_chords (self, section_name: str, progression: "Progression") -> None: 825 826 """Bind a frozen :class:`Progression` to a named form section. 827 828 Every time *section_name* plays, the harmonic clock replays the 829 progression's chords in order instead of calling the live engine. 830 Sections without a bound progression continue generating live chords. 831 832 Parameters: 833 section_name: Name of the section as defined in :meth:`form`. 834 progression: The :class:`Progression` returned by :meth:`freeze`. 835 836 Raises: 837 ValueError: If the form has been configured and *section_name* is 838 not a known section name. 839 840 Example:: 841 842 composition.section_chords("verse", verse_progression) 843 composition.section_chords("chorus", chorus_progression) 844 # "bridge" is not bound — it generates live chords 845 """ 846 847 if ( 848 self._form_state is not None 849 and self._form_state._section_bars is not None 850 and section_name not in self._form_state._section_bars 851 ): 852 known = ", ".join(sorted(self._form_state._section_bars)) 853 raise ValueError( 854 f"Section '{section_name}' not found in form. " 855 f"Known sections: {known}" 856 ) 857 858 self._section_progressions[section_name] = progression 859 860 def on_event (self, event_name: str, callback: typing.Callable[..., typing.Any]) -> None: 861 862 """ 863 Register a callback for a sequencer event (e.g., "bar", "start", "stop"). 864 """ 865 866 self._sequencer.on_event(event_name, callback) 867 868 869 # ----------------------------------------------------------------------- 870 # Hotkey API 871 # ----------------------------------------------------------------------- 872 873 def hotkeys (self, enabled: bool = True) -> None: 874 875 """Enable or disable the global hotkey listener. 876 877 Must be called **before** :meth:`play` to take effect. When enabled, a 878 background thread reads single keystrokes from stdin without requiring 879 Enter. The ``?`` key is always reserved and lists all active bindings. 880 881 Hotkeys have zero impact on playback when disabled — the listener 882 thread is never started. 883 884 Args: 885 enabled: ``True`` (default) to enable hotkeys; ``False`` to disable. 886 887 Example:: 888 889 composition.hotkeys() 890 composition.hotkey("a", lambda: composition.form_jump("chorus")) 891 composition.play() 892 """ 893 894 self._hotkeys_enabled = enabled 895 896 897 def hotkey ( 898 self, 899 key: str, 900 action: typing.Callable[[], None], 901 quantize: int = 0, 902 label: typing.Optional[str] = None, 903 ) -> None: 904 905 """Register a single-key shortcut that fires during playback. 906 907 The listener must be enabled first with :meth:`hotkeys`. 908 909 Most actions — form jumps, ``composition.data`` writes, and 910 :meth:`tweak` calls — should use ``quantize=0`` (the default). Their 911 musical effect is naturally delayed to the next pattern rebuild cycle, 912 which provides automatic musical quantization without extra configuration. 913 914 Use ``quantize=N`` for actions where you want an explicit bar-boundary 915 guarantee, such as :meth:`mute` / :meth:`unmute`. 916 917 The ``?`` key is reserved and cannot be overridden. 918 919 Args: 920 key: A single character trigger (e.g. ``"a"``, ``"1"``, ``" "``). 921 action: Zero-argument callable to execute. 922 quantize: ``0`` = execute immediately (default). ``N`` = execute 923 on the next global bar number divisible by *N*. 924 label: Display name for the ``?`` help listing. Auto-derived from 925 the function name or lambda body if omitted. 926 927 Raises: 928 ValueError: If ``key`` is the reserved ``?`` character, or if 929 ``key`` is not exactly one character. 930 931 Example:: 932 933 composition.hotkeys() 934 935 # Immediate — musical effect happens at next pattern rebuild 936 composition.hotkey("a", lambda: composition.form_jump("chorus")) 937 composition.hotkey("1", lambda: composition.data.update({"mode": "chill"})) 938 939 # Explicit 4-bar phrase boundary 940 composition.hotkey("s", lambda: composition.mute("drums"), quantize=4) 941 942 # Named function — label is derived automatically 943 def drop_to_breakdown(): 944 composition.form_jump("breakdown") 945 composition.mute("lead") 946 947 composition.hotkey("d", drop_to_breakdown) 948 949 composition.play() 950 """ 951 952 if len(key) != 1: 953 raise ValueError(f"hotkey key must be a single character, got {key!r}") 954 955 if key == _HOTKEY_RESERVED: 956 raise ValueError(f"'{_HOTKEY_RESERVED}' is reserved for listing active hotkeys.") 957 958 derived = label if label is not None else _derive_label(action) 959 960 self._hotkey_bindings[key] = HotkeyBinding( 961 key = key, 962 action = action, 963 quantize = quantize, 964 label = derived, 965 ) 966 967 968 def form_jump (self, section_name: str) -> None: 969 970 """Jump the form to a named section immediately. 971 972 Delegates to :meth:`subsequence.form_state.FormState.jump_to`. Only works when the 973 composition uses graph-mode form (a dict passed to :meth:`form`). 974 975 The musical effect is heard at the *next pattern rebuild cycle* — already- 976 queued MIDI notes are unaffected. This natural delay means ``form_jump`` 977 is effective without needing explicit quantization. 978 979 Args: 980 section_name: The section to jump to. 981 982 Raises: 983 ValueError: If no form is configured, or the form is not in graph 984 mode, or *section_name* is unknown. 985 986 Example:: 987 988 composition.hotkey("c", lambda: composition.form_jump("chorus")) 989 """ 990 991 if self._form_state is None: 992 raise ValueError("form_jump() requires a form to be configured via composition.form().") 993 994 self._form_state.jump_to(section_name) 995 996 997 def form_next (self, section_name: str) -> None: 998 999 """Queue the next section — takes effect when the current section ends. 1000 1001 Unlike :meth:`form_jump`, this does not interrupt the current section. 1002 The queued section replaces the automatically pre-decided next section 1003 and takes effect at the natural section boundary. The performer can 1004 change their mind by calling ``form_next`` again before the boundary. 1005 1006 Delegates to :meth:`subsequence.form_state.FormState.queue_next`. Only works when the 1007 composition uses graph-mode form (a dict passed to :meth:`form`). 1008 1009 Args: 1010 section_name: The section to queue. 1011 1012 Raises: 1013 ValueError: If no form is configured, or the form is not in graph 1014 mode, or *section_name* is unknown. 1015 1016 Example:: 1017 1018 composition.hotkey("c", lambda: composition.form_next("chorus")) 1019 """ 1020 1021 if self._form_state is None: 1022 raise ValueError("form_next() requires a form to be configured via composition.form().") 1023 1024 self._form_state.queue_next(section_name) 1025 1026 1027 def _list_hotkeys (self) -> None: 1028 1029 """Log all active hotkey bindings (triggered by the ``?`` key). 1030 1031 Output appears via the standard logger so it scrolls cleanly above 1032 the :class:`~subsequence.display.Display` status line. 1033 """ 1034 1035 lines = ["Active hotkeys:"] 1036 for key in sorted(self._hotkey_bindings): 1037 b = self._hotkey_bindings[key] 1038 quant_str = "immediate" if b.quantize == 0 else f"quantize={b.quantize}" 1039 lines.append(f" {key} \u2192 {b.label} ({quant_str})") 1040 lines.append(f" ? \u2192 list hotkeys") 1041 logger.info("\n".join(lines)) 1042 1043 1044 def _process_hotkeys (self, bar: int) -> None: 1045 1046 """Drain pending keystrokes and execute due actions. 1047 1048 Called on every ``"bar"`` event by the sequencer when hotkeys are 1049 enabled. Handles both immediate (``quantize=0``) and quantized actions. 1050 1051 Immediate actions are executed directly from the keystroke listener 1052 thread (not here). This method only processes quantized actions that 1053 were deferred to a bar boundary. 1054 1055 Args: 1056 bar: The current global bar number from the sequencer. 1057 """ 1058 1059 if self._keystroke_listener is None: 1060 return 1061 1062 # Process newly arrived keys. 1063 for key in self._keystroke_listener.drain(): 1064 1065 if key == _HOTKEY_RESERVED: 1066 self._list_hotkeys() 1067 continue 1068 1069 binding = self._hotkey_bindings.get(key) 1070 if binding is None: 1071 continue 1072 1073 if binding.quantize == 0: 1074 # Immediate — execute now (we're on the bar-event callback, 1075 # which is safe for all mutation methods). 1076 try: 1077 binding.action() 1078 logger.info(f"Hotkey '{key}' \u2192 {binding.label}") 1079 except Exception as exc: 1080 logger.warning(f"Hotkey '{key}' action raised: {exc}") 1081 else: 1082 # Defer until the next quantize boundary. 1083 self._pending_hotkey_actions.append( 1084 _PendingHotkeyAction(binding=binding, requested_bar=bar) 1085 ) 1086 1087 # Fire any pending actions whose bar boundary has arrived. 1088 still_pending: typing.List[_PendingHotkeyAction] = [] 1089 1090 for pending in self._pending_hotkey_actions: 1091 if bar % pending.binding.quantize == 0: 1092 try: 1093 pending.binding.action() 1094 logger.info( 1095 f"Hotkey '{pending.binding.key}' \u2192 {pending.binding.label} " 1096 f"(bar {bar})" 1097 ) 1098 except Exception as exc: 1099 logger.warning( 1100 f"Hotkey '{pending.binding.key}' action raised: {exc}" 1101 ) 1102 else: 1103 still_pending.append(pending) 1104 1105 self._pending_hotkey_actions = still_pending 1106 1107 def seed (self, value: int) -> None: 1108 1109 """ 1110 Set a random seed for deterministic, repeatable playback. 1111 1112 If a seed is set, Subsequence will produce the exact same sequence 1113 every time you run the script. This is vital for finishing tracks or 1114 reproducing a specific 'performance'. 1115 1116 Parameters: 1117 value: An integer seed. 1118 1119 Example: 1120 ```python 1121 # Fix the randomness 1122 comp.seed(42) 1123 ``` 1124 """ 1125 1126 self._seed = value 1127 1128 def display (self, enabled: bool = True, grid: bool = False, grid_scale: float = 1.0) -> None: 1129 1130 """ 1131 Enable or disable the live terminal dashboard. 1132 1133 When enabled, Subsequence uses a safe logging handler that allows a 1134 persistent status line (BPM, Key, Bar, Section, Chord) to stay at 1135 the bottom of the terminal while logs scroll above it. 1136 1137 Parameters: 1138 enabled: Whether to show the display (default True). 1139 grid: When True, render an ASCII grid visualisation of all 1140 running patterns above the status line. The grid updates 1141 once per bar, showing which steps have notes and at what 1142 velocity. 1143 grid_scale: Horizontal zoom factor for the grid (default 1144 ``1.0``). Higher values add visual columns between 1145 grid steps, revealing micro-timing from swing and groove. 1146 Snapped to the nearest integer internally for uniform 1147 marker spacing. 1148 """ 1149 1150 if enabled: 1151 self._display = subsequence.display.Display(self, grid=grid, grid_scale=grid_scale) 1152 else: 1153 self._display = None 1154 1155 def web_ui (self) -> None: 1156 1157 """ 1158 Enable the realtime Web UI Dashboard. 1159 1160 When enabled, Subsequence instantiates a WebSocket server that broadcasts 1161 the current state, signals, and active patterns (with high-res timing and note data) 1162 to any connected browser clients. 1163 """ 1164 1165 self._web_ui_enabled = True 1166 1167 def midi_input (self, device: str, clock_follow: bool = False) -> None: 1168 1169 """ 1170 Configure MIDI input for external sync and MIDI messages. 1171 1172 Parameters: 1173 device: The name of the MIDI input port. 1174 clock_follow: If True, Subsequence will slave its clock to incoming 1175 MIDI Ticks. It will also follow MIDI Start/Stop/Continue 1176 commands. 1177 1178 Example: 1179 ```python 1180 # Slave Subsequence to an external hardware sequencer 1181 comp.midi_input("Scarlett 2i4", clock_follow=True) 1182 ``` 1183 """ 1184 1185 self._input_device = device 1186 self._clock_follow = clock_follow 1187 1188 def clock_output (self, enabled: bool = True) -> None: 1189 1190 """ 1191 Send MIDI timing clock to connected hardware. 1192 1193 When enabled, Subsequence acts as a MIDI clock master and sends 1194 standard clock messages on the output port: a Start message (0xFA) 1195 when playback begins, a Clock tick (0xF8) on every pulse (24 PPQN), 1196 and a Stop message (0xFC) when playback ends. 1197 1198 This allows hardware synthesizers, drum machines, and effect units to 1199 slave their tempo to Subsequence automatically. 1200 1201 **Note:** Clock output is automatically disabled when ``midi_input()`` 1202 is called with ``clock_follow=True``, to prevent a clock feedback loop. 1203 1204 Parameters: 1205 enabled: Whether to send MIDI clock (default True). 1206 1207 Example: 1208 ```python 1209 comp = subsequence.Composition(bpm=120, output_device="...") 1210 comp.clock_output() # hardware will follow Subsequence tempo 1211 ``` 1212 """ 1213 1214 self._clock_output = enabled 1215 1216 1217 def link (self, quantum: float = 4.0) -> "Composition": 1218 1219 """ 1220 Enable Ableton Link tempo and phase synchronisation. 1221 1222 When enabled, Subsequence joins the local Link session and slaves its 1223 clock to the shared network tempo and beat phase. All other Link-enabled 1224 apps on the same LAN — Ableton Live, iOS synths, other Subsequence 1225 instances — will automatically stay in time. 1226 1227 Playback starts on the next bar boundary aligned to the Link quantum, 1228 so downbeats stay in sync across all participants. 1229 1230 Requires the ``link`` optional extra:: 1231 1232 pip install subsequence[link] 1233 1234 Parameters: 1235 quantum: Beat cycle length. ``4.0`` (default) = one bar in 4/4 time. 1236 Change this if your composition uses a different meter. 1237 1238 Example:: 1239 1240 comp = subsequence.Composition(bpm=120, key="C") 1241 comp.link() # join the Link session 1242 comp.play() 1243 1244 # On another machine / instance: 1245 comp2 = subsequence.Composition(bpm=120) 1246 comp2.link() # tempo and phase will lock to comp 1247 comp2.play() 1248 1249 Note: 1250 ``set_bpm()`` proposes the new tempo to the Link network when Link 1251 is active. The network-authoritative tempo is applied on the next 1252 pulse, so there may be a brief lag before the change is visible. 1253 """ 1254 1255 # Eagerly check that aalink is installed — fail early with a clear message. 1256 subsequence.link_clock._require_aalink() 1257 1258 self._link_quantum = quantum 1259 return self 1260 1261 1262 def cc_map ( 1263 self, 1264 cc: int, 1265 key: str, 1266 channel: typing.Optional[int] = None, 1267 min_val: float = 0.0, 1268 max_val: float = 1.0 1269 ) -> None: 1270 1271 """ 1272 Map an incoming MIDI CC to a ``composition.data`` key. 1273 1274 When the composition receives a CC message on the configured MIDI 1275 input port, the value is scaled from the CC range (0–127) to 1276 *[min_val, max_val]* and stored in ``composition.data[key]``. 1277 1278 This lets hardware knobs, faders, and expression pedals control live 1279 parameters without writing any callback code. 1280 1281 **Requires** ``midi_input()`` to be called first to open an input port. 1282 1283 Parameters: 1284 cc: MIDI Control Change number (0–127). 1285 key: The ``composition.data`` key to write. 1286 channel: If given, only respond to CC messages on this channel. 1287 Uses the same numbering convention as ``pattern()`` (0-15 1288 by default, or 1-16 with ``zero_indexed_channels=False``). 1289 ``None`` matches any channel (default). 1290 min_val: Scaled minimum — written when CC value is 0 (default 0.0). 1291 max_val: Scaled maximum — written when CC value is 127 (default 1.0). 1292 1293 Example: 1294 ```python 1295 comp.midi_input("Arturia KeyStep") 1296 comp.cc_map(74, "filter_cutoff") # knob → 0.0–1.0 1297 comp.cc_map(7, "volume", min_val=0, max_val=127) # volume fader 1298 ``` 1299 """ 1300 1301 resolved_channel = self._resolve_channel(channel) if channel is not None else None 1302 1303 self._cc_mappings.append({ 1304 'cc': cc, 1305 'key': key, 1306 'channel': resolved_channel, 1307 'min_val': min_val, 1308 'max_val': max_val, 1309 }) 1310 1311 1312 def live (self, port: int = 5555) -> None: 1313 1314 """ 1315 Enable the live coding eval server. 1316 1317 This allows you to connect to a running composition using the 1318 `subsequence.live_client` REPL and hot-swap pattern code or 1319 modify variables in real-time. 1320 1321 Parameters: 1322 port: The TCP port to listen on (default 5555). 1323 """ 1324 1325 self._live_server = subsequence.live_server.LiveServer(self, port=port) 1326 self._is_live = True 1327 1328 def osc (self, receive_port: int = 9000, send_port: int = 9001, send_host: str = "127.0.0.1") -> None: 1329 1330 """ 1331 Enable bi-directional Open Sound Control (OSC). 1332 1333 Subsequence will listen for commands (like `/bpm` or `/mute`) and 1334 broadcast its internal state (like `/chord` or `/bar`) over UDP. 1335 1336 Parameters: 1337 receive_port: Port to listen for incoming OSC messages (default 9000). 1338 send_port: Port to send state updates to (default 9001). 1339 send_host: The IP address to send updates to (default "127.0.0.1"). 1340 """ 1341 1342 self._osc_server = subsequence.osc.OscServer( 1343 self, 1344 receive_port = receive_port, 1345 send_port = send_port, 1346 send_host = send_host 1347 ) 1348 1349 def set_bpm (self, bpm: float) -> None: 1350 1351 """ 1352 Instantly change the tempo. 1353 1354 Parameters: 1355 bpm: The new tempo in beats per minute. 1356 1357 When Ableton Link is active, this proposes the new tempo to the Link 1358 network instead of applying it locally. The network-authoritative tempo 1359 is picked up on the next pulse. 1360 """ 1361 1362 self._sequencer.set_bpm(bpm) 1363 1364 if not self._clock_follow and self._link_quantum is None: 1365 self.bpm = bpm 1366 1367 def target_bpm (self, bpm: float, bars: int, shape: str = "linear") -> None: 1368 1369 """ 1370 Smoothly ramp the tempo to a target value over a number of bars. 1371 1372 Parameters: 1373 bpm: Target tempo in beats per minute. 1374 bars: Duration of the transition in bars. 1375 shape: Easing curve name. Defaults to ``"linear"``. 1376 ``"ease_in_out"`` or ``"s_curve"`` are recommended for natural- 1377 sounding tempo changes. See :mod:`subsequence.easing` for all 1378 available shapes. 1379 1380 Example: 1381 ```python 1382 # Accelerate to 140 BPM over the next 8 bars with a smooth S-curve 1383 comp.target_bpm(140, bars=8, shape="ease_in_out") 1384 ``` 1385 """ 1386 1387 self._sequencer.set_target_bpm(bpm, bars, shape) 1388 1389 def live_info (self) -> typing.Dict[str, typing.Any]: 1390 1391 """ 1392 Return a dictionary containing the current state of the composition. 1393 1394 Includes BPM, key, current bar, active section, current chord, 1395 running patterns, and custom data. 1396 """ 1397 1398 section_info = None 1399 if self._form_state is not None: 1400 section = self._form_state.get_section_info() 1401 if section is not None: 1402 section_info = { 1403 "name": section.name, 1404 "bar": section.bar, 1405 "bars": section.bars, 1406 "progress": section.progress 1407 } 1408 1409 chord_name = None 1410 if self._harmonic_state is not None: 1411 chord = self._harmonic_state.get_current_chord() 1412 if chord is not None: 1413 chord_name = chord.name() 1414 1415 pattern_list = [] 1416 channel_offset = 0 if self._zero_indexed_channels else 1 1417 for name, pat in self._running_patterns.items(): 1418 pattern_list.append({ 1419 "name": name, 1420 "channel": pat.channel + channel_offset, 1421 "length": pat.length, 1422 "cycle": pat._cycle_count, 1423 "muted": pat._muted, 1424 "tweaks": dict(pat._tweaks) 1425 }) 1426 1427 return { 1428 "bpm": self._sequencer.current_bpm, 1429 "key": self.key, 1430 "bar": self._builder_bar, 1431 "section": section_info, 1432 "chord": chord_name, 1433 "patterns": pattern_list, 1434 "input_device": self._input_device, 1435 "clock_follow": self._clock_follow, 1436 "data": self.data 1437 } 1438 1439 def mute (self, name: str) -> None: 1440 1441 """ 1442 Mute a running pattern by name. 1443 1444 The pattern continues to 'run' and increment its cycle count in 1445 the background, but it will not produce any MIDI notes until unmuted. 1446 1447 Parameters: 1448 name: The function name of the pattern to mute. 1449 """ 1450 1451 if name not in self._running_patterns: 1452 raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}") 1453 1454 self._running_patterns[name]._muted = True 1455 logger.info(f"Muted pattern: {name}") 1456 1457 def unmute (self, name: str) -> None: 1458 1459 """ 1460 Unmute a previously muted pattern. 1461 """ 1462 1463 if name not in self._running_patterns: 1464 raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}") 1465 1466 self._running_patterns[name]._muted = False 1467 logger.info(f"Unmuted pattern: {name}") 1468 1469 def tweak (self, name: str, **kwargs: typing.Any) -> None: 1470 1471 """Override parameters for a running pattern. 1472 1473 Values set here are available inside the pattern's builder 1474 function via ``p.param()``. They persist across rebuilds 1475 until explicitly changed or cleared. Changes take effect 1476 on the next rebuild cycle. 1477 1478 Parameters: 1479 name: The function name of the pattern. 1480 **kwargs: Parameter names and their new values. 1481 1482 Example (from the live REPL):: 1483 1484 composition.tweak("bass", pitches=[48, 52, 55, 60]) 1485 """ 1486 1487 if name not in self._running_patterns: 1488 raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}") 1489 1490 self._running_patterns[name]._tweaks.update(kwargs) 1491 logger.info(f"Tweaked pattern '{name}': {list(kwargs.keys())}") 1492 1493 def clear_tweak (self, name: str, *param_names: str) -> None: 1494 1495 """Remove tweaked parameters from a running pattern. 1496 1497 If no parameter names are given, all tweaks for the pattern 1498 are cleared and every ``p.param()`` call reverts to its 1499 default. 1500 1501 Parameters: 1502 name: The function name of the pattern. 1503 *param_names: Specific parameter names to clear. If 1504 omitted, all tweaks are removed. 1505 """ 1506 1507 if name not in self._running_patterns: 1508 raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}") 1509 1510 if not param_names: 1511 self._running_patterns[name]._tweaks.clear() 1512 logger.info(f"Cleared all tweaks for pattern '{name}'") 1513 else: 1514 for param_name in param_names: 1515 self._running_patterns[name]._tweaks.pop(param_name, None) 1516 logger.info(f"Cleared tweaks for pattern '{name}': {list(param_names)}") 1517 1518 def get_tweaks (self, name: str) -> typing.Dict[str, typing.Any]: 1519 1520 """Return a copy of the current tweaks for a running pattern. 1521 1522 Parameters: 1523 name: The function name of the pattern. 1524 """ 1525 1526 if name not in self._running_patterns: 1527 raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}") 1528 1529 return dict(self._running_patterns[name]._tweaks) 1530 1531 def schedule (self, fn: typing.Callable, cycle_beats: int, reschedule_lookahead: int = 1, wait_for_initial: bool = False, defer: bool = False) -> None: 1532 1533 """ 1534 Register a custom function to run on a repeating beat-based cycle. 1535 1536 Subsequence automatically runs synchronous functions in a thread pool 1537 so they don't block the timing-critical MIDI clock. Async functions 1538 are run directly on the event loop. 1539 1540 Parameters: 1541 fn: The function to call. 1542 cycle_beats: How often to call it (e.g., 4 = every bar). 1543 reschedule_lookahead: How far in advance to schedule the next call. 1544 wait_for_initial: If True, run the function once during startup 1545 and wait for it to complete before playback begins. This 1546 ensures ``composition.data`` is populated before patterns 1547 first build. Implies ``defer=True`` for the repeating 1548 schedule. 1549 defer: If True, skip the pulse-0 fire and defer the first 1550 repeating call to just before the second cycle boundary. 1551 """ 1552 1553 self._pending_scheduled.append(_PendingScheduled(fn, cycle_beats, reschedule_lookahead, wait_for_initial, defer)) 1554 1555 def form ( 1556 self, 1557 sections: typing.Union[ 1558 typing.List[typing.Tuple[str, int]], 1559 typing.Iterator[typing.Tuple[str, int]], 1560 typing.Dict[str, typing.Tuple[int, typing.Optional[typing.List[typing.Tuple[str, int]]]]] 1561 ], 1562 loop: bool = False, 1563 start: typing.Optional[str] = None 1564 ) -> None: 1565 1566 """ 1567 Define the structure (sections) of the composition. 1568 1569 You can define form in three ways: 1570 1. **Graph (Dict)**: Dynamic transitions based on weights. 1571 2. **Sequence (List)**: A fixed order of sections. 1572 3. **Generator**: A Python generator that yields `(name, bars)` pairs. 1573 1574 Parameters: 1575 sections: The form definition (Dict, List, or Generator). 1576 loop: Whether to cycle back to the start (List mode only). 1577 start: The section to start with (Graph mode only). 1578 1579 Example: 1580 ```python 1581 # A simple pop structure 1582 comp.form([ 1583 ("verse", 8), 1584 ("chorus", 8), 1585 ("verse", 8), 1586 ("chorus", 16) 1587 ]) 1588 ``` 1589 """ 1590 1591 self._form_state = subsequence.form_state.FormState(sections, loop=loop, start=start) 1592 1593 def pattern ( 1594 self, 1595 channel: int, 1596 length: float = 4, 1597 unit: typing.Optional[float] = None, 1598 drum_note_map: typing.Optional[typing.Dict[str, int]] = None, 1599 reschedule_lookahead: float = 1, 1600 voice_leading: bool = False 1601 ) -> typing.Callable: 1602 1603 """ 1604 Register a function as a repeating MIDI pattern. 1605 1606 The decorated function will be called once per cycle to 'rebuild' its 1607 content. This allows for generative logic that evolves over time. 1608 1609 When ``unit`` is provided, ``length`` is a note count and the actual 1610 duration in beats is ``length * unit``. The note count also becomes 1611 the default grid size for ``hit_steps()`` and ``sequence()``. 1612 1613 When ``unit`` is omitted, ``length`` is in beats (quarter notes) and 1614 the grid defaults to sixteenth-note resolution. 1615 1616 Parameters: 1617 channel: MIDI channel. By default uses 0-based numbering (0-15) 1618 matching the raw MIDI protocol. Set 1619 ``zero_indexed_channels=False`` on the ``Composition`` to use 1620 1-based numbering (1-16) instead. 1621 length: Note count when ``unit`` is given, otherwise duration 1622 in beats (default 4). 1623 unit: Duration of one note in beats (e.g. ``dur.SIXTEENTH``). 1624 When set, ``length`` is treated as a count and the grid 1625 defaults to ``length``. 1626 drum_note_map: Optional mapping for drum instruments. 1627 reschedule_lookahead: Beats in advance to compute the next cycle. 1628 voice_leading: If True, chords in this pattern will automatically 1629 use inversions that minimize voice movement. 1630 1631 Example: 1632 ```python 1633 @comp.pattern(channel=1, length=6, unit=dur.SIXTEENTH) 1634 def riff(p): 1635 p.sequence(steps=[0, 1, 3, 5], pitches=60) 1636 ``` 1637 """ 1638 1639 channel = self._resolve_channel(channel) 1640 1641 if unit is not None: 1642 beat_length = length * unit 1643 default_grid = int(length) 1644 else: 1645 beat_length = length 1646 default_grid = round(beat_length / subsequence.constants.durations.SIXTEENTH) 1647 1648 def decorator (fn: typing.Callable) -> typing.Callable: 1649 1650 """ 1651 Wrap the builder function and register it as a pending pattern. 1652 During live sessions, hot-swap an existing pattern's builder instead. 1653 """ 1654 1655 # Hot-swap: if we're live and a pattern with this name exists, replace its builder. 1656 if self._is_live and fn.__name__ in self._running_patterns: 1657 running = self._running_patterns[fn.__name__] 1658 running._builder_fn = fn 1659 running._wants_chord = _fn_has_parameter(fn, "chord") 1660 logger.info(f"Hot-swapped pattern: {fn.__name__}") 1661 return fn 1662 1663 pending = _PendingPattern( 1664 builder_fn = fn, 1665 channel = channel, # already resolved to 0-indexed 1666 length = beat_length, 1667 default_grid = default_grid, 1668 drum_note_map = drum_note_map, 1669 reschedule_lookahead = reschedule_lookahead, 1670 voice_leading = voice_leading 1671 ) 1672 1673 self._pending_patterns.append(pending) 1674 1675 return fn 1676 1677 return decorator 1678 1679 def layer ( 1680 self, 1681 *builder_fns: typing.Callable, 1682 channel: int, 1683 length: float = 4, 1684 unit: typing.Optional[float] = None, 1685 drum_note_map: typing.Optional[typing.Dict[str, int]] = None, 1686 reschedule_lookahead: float = 1, 1687 voice_leading: bool = False 1688 ) -> None: 1689 1690 """ 1691 Combine multiple functions into a single MIDI pattern. 1692 1693 This is useful for composing complex patterns out of reusable 1694 building blocks (e.g., a 'kick' function and a 'snare' function). 1695 1696 Parameters: 1697 builder_fns: One or more pattern builder functions. 1698 channel: MIDI channel (0-15, or 1-16 with ``zero_indexed_channels=False``). 1699 length: Note count when ``unit`` is given, otherwise duration 1700 in beats (default 4). 1701 unit: Duration of one note in beats (e.g. ``dur.SIXTEENTH``). 1702 When set, ``length`` is treated as a count and the grid 1703 defaults to ``length``. 1704 drum_note_map: Optional mapping for drum instruments. 1705 reschedule_lookahead: Beats in advance to compute the next cycle. 1706 voice_leading: If True, chords use smooth voice leading. 1707 """ 1708 1709 if unit is not None: 1710 beat_length = length * unit 1711 default_grid = int(length) 1712 else: 1713 beat_length = length 1714 default_grid = round(beat_length / subsequence.constants.durations.SIXTEENTH) 1715 1716 wants_chord = any(_fn_has_parameter(fn, "chord") for fn in builder_fns) 1717 1718 if wants_chord: 1719 1720 def merged_builder (p: subsequence.pattern_builder.PatternBuilder, chord: _InjectedChord) -> None: 1721 1722 for fn in builder_fns: 1723 if _fn_has_parameter(fn, "chord"): 1724 fn(p, chord) 1725 else: 1726 fn(p) 1727 1728 else: 1729 1730 def merged_builder (p: subsequence.pattern_builder.PatternBuilder) -> None: # type: ignore[misc] 1731 1732 for fn in builder_fns: 1733 fn(p) 1734 1735 resolved = self._resolve_channel(channel) 1736 1737 pending = _PendingPattern( 1738 builder_fn = merged_builder, 1739 channel = resolved, # already resolved to 0-indexed 1740 length = beat_length, 1741 default_grid = default_grid, 1742 drum_note_map = drum_note_map, 1743 reschedule_lookahead = reschedule_lookahead, 1744 voice_leading = voice_leading 1745 ) 1746 1747 self._pending_patterns.append(pending) 1748 1749 def trigger ( 1750 self, 1751 fn: typing.Callable, 1752 channel: int, 1753 length: float = 1, 1754 quantize: float = 0, 1755 drum_note_map: typing.Optional[typing.Dict[str, int]] = None, 1756 chord: bool = False 1757 ) -> None: 1758 1759 """ 1760 Trigger a one-shot pattern immediately or on a quantized boundary. 1761 1762 This is useful for real-time response to sensors, OSC messages, or other 1763 external events. The builder function is called immediately with a fresh 1764 PatternBuilder, and the generated events are injected into the queue at 1765 the specified quantize boundary. 1766 1767 The builder function has the same API as a ``@composition.pattern`` 1768 decorated function and can use all PatternBuilder methods: ``p.note()``, 1769 ``p.euclidean()``, ``p.arpeggio()``, and so on. 1770 1771 Parameters: 1772 fn: The pattern builder function (same signature as ``@comp.pattern``). 1773 channel: MIDI channel (0-15, or 1-16 with ``zero_indexed_channels=False``). 1774 length: Duration in beats (default 1). This is the time window for the 1775 one-shot pattern. 1776 quantize: Snap the trigger to a beat boundary: ``0`` = immediate (default), 1777 ``1`` = next beat (quarter note), ``4`` = next bar. Use ``dur.*`` 1778 constants from ``subsequence.constants.durations``. 1779 drum_note_map: Optional drum name mapping for this pattern. 1780 chord: If ``True``, the builder function receives the current chord as 1781 a second parameter (same as ``@composition.pattern``). 1782 1783 Example: 1784 ```python 1785 # Immediate single note 1786 composition.trigger( 1787 lambda p: p.note(60, beat=0, velocity=100, duration=0.5), 1788 channel=0 1789 ) 1790 1791 # Quantized fill (next bar) 1792 import subsequence.constants.durations as dur 1793 composition.trigger( 1794 lambda p: p.euclidean("snare", pulses=7, velocity=90), 1795 channel=9, 1796 drum_note_map=gm_drums.GM_DRUM_MAP, 1797 quantize=dur.WHOLE 1798 ) 1799 1800 # With chord context 1801 composition.trigger( 1802 lambda p: p.arpeggio(p.chord.tones(root=60), step=dur.SIXTEENTH), 1803 channel=0, 1804 quantize=dur.QUARTER, 1805 chord=True 1806 ) 1807 ``` 1808 """ 1809 1810 # Resolve channel numbering 1811 resolved_channel = self._resolve_channel(channel) 1812 1813 # Create a temporary Pattern 1814 pattern = subsequence.pattern.Pattern(channel=resolved_channel, length=length) 1815 1816 # Create a PatternBuilder 1817 builder = subsequence.pattern_builder.PatternBuilder( 1818 pattern=pattern, 1819 cycle=0, # One-shot patterns don't rebuild, so cycle is always 0 1820 drum_note_map=drum_note_map, 1821 section=self._form_state.get_section_info() if self._form_state else None, 1822 bar=self._builder_bar, 1823 conductor=self.conductor, 1824 rng=random.Random(), # Fresh random state for each trigger 1825 tweaks={}, 1826 default_grid=round(length / subsequence.constants.durations.SIXTEENTH), 1827 data=self.data 1828 ) 1829 1830 # Call the builder function 1831 try: 1832 1833 if chord and self._harmonic_state is not None: 1834 current_chord = self._harmonic_state.get_current_chord() 1835 injected = _InjectedChord(current_chord, None) # No voice leading for one-shots 1836 fn(builder, injected) 1837 1838 else: 1839 fn(builder) 1840 1841 except Exception: 1842 logger.exception("Error in trigger builder — pattern will be silent") 1843 return 1844 1845 # Calculate the start pulse based on quantize 1846 current_pulse = self._sequencer.pulse_count 1847 pulses_per_beat = subsequence.constants.MIDI_QUARTER_NOTE 1848 1849 if quantize == 0: 1850 # Immediate: use current pulse 1851 start_pulse = current_pulse 1852 1853 else: 1854 # Quantize to the next multiple of (quantize * pulses_per_beat) 1855 quantize_pulses = int(quantize * pulses_per_beat) 1856 start_pulse = ((current_pulse // quantize_pulses) + 1) * quantize_pulses 1857 1858 # Schedule the pattern for one-shot execution 1859 try: 1860 loop = asyncio.get_running_loop() 1861 # Already on the event loop 1862 asyncio.create_task(self._sequencer.schedule_pattern(pattern, start_pulse)) 1863 1864 except RuntimeError: 1865 # Not on the event loop — schedule via call_soon_threadsafe 1866 if self._sequencer._event_loop is not None: 1867 asyncio.run_coroutine_threadsafe( 1868 self._sequencer.schedule_pattern(pattern, start_pulse), 1869 loop=self._sequencer._event_loop 1870 ) 1871 else: 1872 logger.warning("trigger() called before playback started; pattern ignored") 1873 1874 def play (self) -> None: 1875 1876 """ 1877 Start the composition. 1878 1879 This call blocks until the program is interrupted (e.g., via Ctrl+C). 1880 It initializes the MIDI hardware, launches the background sequencer, 1881 and begins playback. 1882 """ 1883 1884 try: 1885 asyncio.run(self._run()) 1886 1887 except KeyboardInterrupt: 1888 pass 1889 1890 1891 def render (self, bars: typing.Optional[int] = None, filename: str = "render.mid", max_minutes: typing.Optional[float] = 60.0) -> None: 1892 1893 """Render the composition to a MIDI file without real-time playback. 1894 1895 Runs the sequencer as fast as possible (no timing delays) and stops 1896 when the first active limit is reached. The result is saved as a 1897 standard MIDI file that can be imported into any DAW. 1898 1899 All patterns, scheduled callbacks, and harmony logic run exactly as 1900 they would during live playback — BPM transitions, generative fills, 1901 and probabilistic gates all work in render mode. The only difference 1902 is that time is simulated rather than wall-clock driven. 1903 1904 Parameters: 1905 bars: Number of bars to render, or ``None`` for no bar limit 1906 (default ``None``). When both *bars* and *max_minutes* are 1907 active, playback stops at whichever limit is reached first. 1908 filename: Output MIDI filename (default ``"render.mid"``). 1909 max_minutes: Safety cap on the length of rendered MIDI in minutes 1910 (default ``60.0``). Pass ``None`` to disable the time 1911 cap — you must then provide an explicit *bars* value. 1912 1913 Raises: 1914 ValueError: If both *bars* and *max_minutes* are ``None``, which 1915 would produce an infinite render. 1916 1917 Examples: 1918 ```python 1919 # Default: renders up to 60 minutes of MIDI content. 1920 composition.render() 1921 1922 # Render exactly 64 bars (time cap still active as backstop). 1923 composition.render(bars=64, filename="demo.mid") 1924 1925 # Render up to 5 minutes of an infinite generative composition. 1926 composition.render(max_minutes=5, filename="five_min.mid") 1927 1928 # Remove the time cap — must supply bars instead. 1929 composition.render(bars=128, max_minutes=None, filename="long.mid") 1930 ``` 1931 """ 1932 1933 if bars is None and max_minutes is None: 1934 raise ValueError( 1935 "render() requires at least one limit: provide bars=, max_minutes=, or both. " 1936 "Passing both as None would produce an infinite render." 1937 ) 1938 1939 self._sequencer.recording = True 1940 self._sequencer.record_filename = filename 1941 self._sequencer.render_mode = True 1942 self._sequencer.render_bars = bars if bars is not None else 0 1943 self._sequencer.render_max_seconds = max_minutes * 60.0 if max_minutes is not None else None 1944 asyncio.run(self._run()) 1945 1946 async def _run (self) -> None: 1947 1948 """ 1949 Async entry point that schedules all patterns and runs the sequencer. 1950 """ 1951 1952 # Pass MIDI input configuration to the sequencer before start. 1953 if self._input_device is not None: 1954 self._sequencer.input_device_name = self._input_device 1955 self._sequencer.clock_follow = self._clock_follow 1956 1957 # Pass clock output flag (suppressed automatically when clock_follow=True). 1958 self._sequencer.clock_output = self._clock_output and not self._clock_follow 1959 1960 # Create Ableton Link clock if comp.link() was called. 1961 if self._link_quantum is not None: 1962 self._sequencer._link_clock = subsequence.link_clock.LinkClock( 1963 bpm = self.bpm, 1964 quantum = self._link_quantum, 1965 loop = asyncio.get_running_loop(), 1966 ) 1967 1968 # Share CC input mappings and a reference to composition.data with the sequencer. 1969 self._sequencer.cc_mappings = self._cc_mappings 1970 self._sequencer._composition_data = self.data 1971 1972 # Derive child RNGs from the master seed so each component gets 1973 # an independent, deterministic stream. When no seed is set, 1974 # each component creates its own unseeded RNG (existing behaviour). 1975 self._pattern_rngs: typing.List[random.Random] = [] 1976 1977 if self._seed is not None: 1978 master = random.Random(self._seed) 1979 1980 if self._harmonic_state is not None: 1981 self._harmonic_state.rng = random.Random(master.randint(0, 2 ** 63)) 1982 1983 if self._form_state is not None: 1984 self._form_state._rng = random.Random(master.randint(0, 2 ** 63)) 1985 1986 for _ in self._pending_patterns: 1987 self._pattern_rngs.append(random.Random(master.randint(0, 2 ** 63))) 1988 1989 if self._harmonic_state is not None and self._harmony_cycle_beats is not None: 1990 1991 def _get_section_progression () -> typing.Optional[typing.Tuple[str, int, typing.Optional[Progression]]]: 1992 """Return (section_name, section_index, Progression|None) for the current section, or None.""" 1993 if self._form_state is None: 1994 return None 1995 info = self._form_state.get_section_info() 1996 if info is None: 1997 return None 1998 prog = self._section_progressions.get(info.name) 1999 return (info.name, info.index, prog) 2000 2001 await schedule_harmonic_clock( 2002 sequencer = self._sequencer, 2003 get_harmonic_state = lambda: self._harmonic_state, 2004 cycle_beats = self._harmony_cycle_beats, 2005 reschedule_lookahead = self._harmony_reschedule_lookahead, 2006 get_section_progression = _get_section_progression, 2007 ) 2008 2009 if self._form_state is not None: 2010 2011 await schedule_form( 2012 sequencer = self._sequencer, 2013 form_state = self._form_state, 2014 reschedule_lookahead = 1 2015 ) 2016 2017 # Bar counter - always active so p.bar is available to all builders. 2018 def _advance_builder_bar (pulse: int) -> None: 2019 self._builder_bar += 1 2020 2021 first_bar_pulse = int(self.time_signature[0] * self._sequencer.pulses_per_beat) 2022 2023 await self._sequencer.schedule_callback_repeating( 2024 callback = _advance_builder_bar, 2025 interval_beats = self.time_signature[0], 2026 start_pulse = first_bar_pulse, 2027 reschedule_lookahead = 1 2028 ) 2029 2030 # Run wait_for_initial=True scheduled functions and block until all complete. 2031 # This ensures composition.data is populated before patterns build. 2032 initial_tasks = [t for t in self._pending_scheduled if t.wait_for_initial] 2033 2034 if initial_tasks: 2035 2036 names = ", ".join(t.fn.__name__ for t in initial_tasks) 2037 logger.info(f"Waiting for initial scheduled {'function' if len(initial_tasks) == 1 else 'functions'} before start: {names}") 2038 2039 async def _run_initial (fn: typing.Callable) -> None: 2040 2041 accepts_ctx = _fn_has_parameter(fn, "p") 2042 ctx = ScheduleContext(cycle=0) 2043 2044 try: 2045 if asyncio.iscoroutinefunction(fn): 2046 await (fn(ctx) if accepts_ctx else fn()) 2047 else: 2048 loop = asyncio.get_running_loop() 2049 call = (lambda: fn(ctx)) if accepts_ctx else fn 2050 await loop.run_in_executor(None, call) 2051 except Exception as exc: 2052 logger.warning(f"Initial run of {fn.__name__!r} failed: {exc}") 2053 2054 await asyncio.gather(*[_run_initial(t.fn) for t in initial_tasks]) 2055 2056 for pending_task in self._pending_scheduled: 2057 2058 accepts_ctx = _fn_has_parameter(pending_task.fn, "p") 2059 wrapped = _make_safe_callback(pending_task.fn, accepts_context=accepts_ctx) 2060 2061 # wait_for_initial=True implies defer — no point firing at pulse 0 2062 # after the blocking run just completed. defer=True skips the 2063 # backshift fire so the first repeating call happens one full cycle 2064 # later. 2065 if pending_task.wait_for_initial or pending_task.defer: 2066 start_pulse = int(pending_task.cycle_beats * self._sequencer.pulses_per_beat) 2067 else: 2068 start_pulse = 0 2069 2070 await self._sequencer.schedule_callback_repeating( 2071 callback = wrapped, 2072 interval_beats = pending_task.cycle_beats, 2073 start_pulse = start_pulse, 2074 reschedule_lookahead = pending_task.reschedule_lookahead 2075 ) 2076 2077 # Build Pattern objects from pending registrations. 2078 patterns: typing.List[subsequence.pattern.Pattern] = [] 2079 2080 for i, pending in enumerate(self._pending_patterns): 2081 2082 pattern_rng = self._pattern_rngs[i] if i < len(self._pattern_rngs) else None 2083 pattern = self._build_pattern_from_pending(pending, pattern_rng) 2084 patterns.append(pattern) 2085 2086 await schedule_patterns( 2087 sequencer = self._sequencer, 2088 patterns = patterns, 2089 start_pulse = 0 2090 ) 2091 2092 # Populate the running patterns dict for live hot-swap and mute/unmute. 2093 for i, pending in enumerate(self._pending_patterns): 2094 name = pending.builder_fn.__name__ 2095 self._running_patterns[name] = patterns[i] 2096 2097 if self._display is not None and not self._sequencer.render_mode: 2098 self._display.start() 2099 self._sequencer.on_event("bar", self._display.update) 2100 self._sequencer.on_event("beat", self._display.update) 2101 2102 if self._live_server is not None: 2103 await self._live_server.start() 2104 2105 if self._osc_server is not None: 2106 await self._osc_server.start() 2107 self._sequencer.osc_server = self._osc_server 2108 2109 def _send_osc_status (bar: int) -> None: 2110 if self._osc_server: 2111 self._osc_server.send("/bar", bar) 2112 self._osc_server.send("/bpm", self._sequencer.current_bpm) 2113 2114 if self._harmonic_state: 2115 self._osc_server.send("/chord", self._harmonic_state.current_chord.name()) 2116 2117 if self._form_state: 2118 info = self._form_state.get_section_info() 2119 if info: 2120 self._osc_server.send("/section", info.name) 2121 2122 self._sequencer.on_event("bar", _send_osc_status) 2123 2124 # Start keystroke listener if hotkeys are enabled and not in render mode. 2125 if self._hotkeys_enabled and not self._sequencer.render_mode: 2126 self._keystroke_listener = subsequence.keystroke.KeystrokeListener() 2127 self._keystroke_listener.start() 2128 2129 if self._keystroke_listener.active: 2130 # Listener started successfully — register the bar handler 2131 # and show all bindings so the user knows what's available. 2132 self._sequencer.on_event("bar", self._process_hotkeys) 2133 self._list_hotkeys() 2134 # If not active, KeystrokeListener.start() already logged a warning. 2135 2136 if self._web_ui_enabled and not self._sequencer.render_mode: 2137 self._web_ui_server = subsequence.web_ui.WebUI(self) 2138 self._web_ui_server.start() 2139 2140 await run_until_stopped(self._sequencer) 2141 2142 if self._web_ui_server is not None: 2143 self._web_ui_server.stop() 2144 2145 if self._live_server is not None: 2146 await self._live_server.stop() 2147 2148 if self._osc_server is not None: 2149 await self._osc_server.stop() 2150 self._sequencer.osc_server = None 2151 2152 if self._display is not None: 2153 self._display.stop() 2154 2155 if self._keystroke_listener is not None: 2156 self._keystroke_listener.stop() 2157 self._keystroke_listener = None 2158 2159 def _build_pattern_from_pending (self, pending: _PendingPattern, rng: typing.Optional[random.Random] = None) -> subsequence.pattern.Pattern: 2160 2161 """ 2162 Create a Pattern from a pending registration using a temporary subclass. 2163 """ 2164 2165 composition_ref = self 2166 2167 class _DecoratorPattern (subsequence.pattern.Pattern): 2168 2169 """ 2170 Pattern subclass that delegates to a builder function on each reschedule. 2171 """ 2172 2173 def __init__ (self, pending: _PendingPattern, pattern_rng: typing.Optional[random.Random] = None) -> None: 2174 2175 """ 2176 Initialize the decorator pattern from pending registration details. 2177 """ 2178 2179 super().__init__( 2180 channel = pending.channel, 2181 length = pending.length, 2182 reschedule_lookahead = min( 2183 pending.reschedule_lookahead, 2184 composition_ref._harmony_reschedule_lookahead 2185 ) 2186 ) 2187 2188 self._builder_fn = pending.builder_fn 2189 self._drum_note_map = pending.drum_note_map 2190 self._default_grid: int = pending.default_grid 2191 self._wants_chord = _fn_has_parameter(pending.builder_fn, "chord") 2192 self._cycle_count = 0 2193 self._rng = pattern_rng 2194 self._muted = False 2195 self._voice_leading_state: typing.Optional[subsequence.voicings.VoiceLeadingState] = ( 2196 subsequence.voicings.VoiceLeadingState() if pending.voice_leading else None 2197 ) 2198 self._tweaks: typing.Dict[str, typing.Any] = {} 2199 2200 self._rebuild() 2201 2202 def _rebuild (self) -> None: 2203 2204 """ 2205 Clear steps and call the builder function to repopulate. 2206 """ 2207 2208 self.steps = {} 2209 self.cc_events = [] 2210 self.osc_events = [] 2211 current_cycle = self._cycle_count 2212 self._cycle_count += 1 2213 2214 if self._muted: 2215 return 2216 2217 builder = subsequence.pattern_builder.PatternBuilder( 2218 pattern = self, 2219 cycle = current_cycle, 2220 drum_note_map = self._drum_note_map, 2221 section = composition_ref._form_state.get_section_info() if composition_ref._form_state else None, 2222 bar = composition_ref._builder_bar, 2223 conductor = composition_ref.conductor, 2224 rng = self._rng, 2225 tweaks = self._tweaks, 2226 default_grid = self._default_grid, 2227 data = composition_ref.data 2228 ) 2229 2230 try: 2231 2232 if self._wants_chord and composition_ref._harmonic_state is not None: 2233 chord = composition_ref._harmonic_state.get_current_chord() 2234 injected = _InjectedChord(chord, self._voice_leading_state) 2235 self._builder_fn(builder, injected) 2236 2237 else: 2238 self._builder_fn(builder) 2239 2240 except Exception: 2241 logger.exception("Error in pattern builder '%s' (cycle %d) - pattern will be silent this cycle", self._builder_fn.__name__, current_cycle) 2242 2243 def on_reschedule (self) -> None: 2244 2245 """ 2246 Rebuild the pattern from the builder function before the next cycle. 2247 """ 2248 2249 self._rebuild() 2250 2251 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.
567 def __init__ ( 568 self, 569 output_device: typing.Optional[str] = None, 570 bpm: float = 120, 571 time_signature: typing.Tuple[int, int] = (4, 4), 572 key: typing.Optional[str] = None, 573 seed: typing.Optional[int] = None, 574 record: bool = False, 575 record_filename: typing.Optional[str] = None, 576 zero_indexed_channels: bool = True 577 ) -> None: 578 579 """ 580 Initialize a new composition. 581 582 Parameters: 583 output_device: The name of the MIDI output port to use. If `None`, 584 Subsequence will attempt to find a device, prompting if necessary. 585 bpm: Initial tempo in beats per minute (default 120). 586 key: The root key of the piece (e.g., "C", "F#", "Bb"). 587 Required if you plan to use `harmony()`. 588 seed: An optional integer for deterministic randomness. When set, 589 every random decision (chord choices, drum probability, etc.) 590 will be identical on every run. 591 record: When True, record all MIDI events to a file. 592 record_filename: Optional filename for the recording (defaults to timestamp). 593 zero_indexed_channels: When False, MIDI channels use 594 1-based numbering (1-16) matching instrument labelling. 595 Channel 10 is drums, the way musicians and hardware panels 596 show it. When True (default), channels use 0-based numbering (0-15) 597 matching the raw MIDI protocol. 598 599 Example: 600 ```python 601 comp = subsequence.Composition(bpm=128, key="Eb", seed=123) 602 ``` 603 """ 604 605 self.output_device = output_device 606 self.bpm = bpm 607 self.time_signature = time_signature 608 self.key = key 609 self._seed: typing.Optional[int] = seed 610 self._zero_indexed_channels: bool = zero_indexed_channels 611 612 self._sequencer = subsequence.sequencer.Sequencer( 613 output_device_name = output_device, 614 initial_bpm = bpm, 615 time_signature = time_signature, 616 record = record, 617 record_filename = record_filename 618 ) 619 620 self._harmonic_state: typing.Optional[subsequence.harmonic_state.HarmonicState] = None 621 self._harmony_cycle_beats: typing.Optional[int] = None 622 self._harmony_reschedule_lookahead: float = 1 623 self._section_progressions: typing.Dict[str, Progression] = {} 624 self._pending_patterns: typing.List[_PendingPattern] = [] 625 self._pending_scheduled: typing.List[_PendingScheduled] = [] 626 self._form_state: typing.Optional[subsequence.form_state.FormState] = None 627 self._builder_bar: int = 0 628 self._display: typing.Optional[subsequence.display.Display] = None 629 self._live_server: typing.Optional[subsequence.live_server.LiveServer] = None 630 self._is_live: bool = False 631 self._running_patterns: typing.Dict[str, typing.Any] = {} 632 self._input_device: typing.Optional[str] = None 633 self._clock_follow: bool = False 634 self._clock_output: bool = False 635 self._cc_mappings: typing.List[typing.Dict[str, typing.Any]] = [] 636 self.data: typing.Dict[str, typing.Any] = {} 637 self._osc_server: typing.Optional[subsequence.osc.OscServer] = None 638 self.conductor = subsequence.conductor.Conductor() 639 self._web_ui_enabled: bool = False 640 self._web_ui_server: typing.Optional[subsequence.web_ui.WebUI] = None 641 self._link_quantum: typing.Optional[float] = None 642 643 # Hotkey state — populated by hotkeys() and hotkey(). 644 self._hotkeys_enabled: bool = False 645 self._hotkey_bindings: typing.Dict[str, HotkeyBinding] = {} 646 self._pending_hotkey_actions: typing.List[_PendingHotkeyAction] = [] 647 self._keystroke_listener: typing.Optional[subsequence.keystroke.KeystrokeListener] = None
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, 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 (default), channels use 0-based numbering (0-15) matching the raw MIDI protocol.
Example:
comp = subsequence.Composition(bpm=128, key="Eb", seed=123)
668 @property 669 def harmonic_state (self) -> typing.Optional[subsequence.harmonic_state.HarmonicState]: 670 """The active ``HarmonicState``, or ``None`` if ``harmony()`` has not been called.""" 671 return self._harmonic_state
The active HarmonicState, or None if harmony() has not been called.
673 @property 674 def form_state (self) -> typing.Optional["subsequence.form_state.FormState"]: 675 """The active ``subsequence.form_state.FormState``, or ``None`` if ``form()`` has not been called.""" 676 return self._form_state
The active subsequence.form_state.FormState, or None if form() has not been called.
678 @property 679 def sequencer (self) -> subsequence.sequencer.Sequencer: 680 """The underlying ``Sequencer`` instance.""" 681 return self._sequencer
The underlying Sequencer instance.
683 @property 684 def running_patterns (self) -> typing.Dict[str, typing.Any]: 685 """The currently active patterns, keyed by name.""" 686 return self._running_patterns
The currently active patterns, keyed by name.
688 @property 689 def builder_bar (self) -> int: 690 """Current bar index used by pattern builders.""" 691 return self._builder_bar
Current bar index used by pattern builders.
702 def harmony ( 703 self, 704 style: typing.Union[str, subsequence.chord_graphs.ChordGraph] = "functional_major", 705 cycle_beats: int = 4, 706 dominant_7th: bool = True, 707 gravity: float = 1.0, 708 nir_strength: float = 0.5, 709 minor_turnaround_weight: float = 0.0, 710 root_diversity: float = subsequence.harmonic_state.DEFAULT_ROOT_DIVERSITY, 711 reschedule_lookahead: float = 1 712 ) -> None: 713 714 """ 715 Configure the harmonic logic and chord change intervals. 716 717 Subsequence uses a weighted transition graph to choose the next chord. 718 You can influence these choices using 'gravity' (favoring the tonic) and 719 'NIR strength' (melodic inertia based on Narmour's model). 720 721 Parameters: 722 style: The harmonic style to use. Built-in: "functional_major" 723 (alias "diatonic_major"), "turnaround", "aeolian_minor", 724 "phrygian_minor", "lydian_major", "dorian_minor", 725 "chromatic_mediant", "suspended", "mixolydian", "whole_tone", 726 "diminished". See README for full descriptions. 727 cycle_beats: How many beats each chord lasts (default 4). 728 dominant_7th: Whether to include V7 chords (default True). 729 gravity: Key gravity (0.0 to 1.0). High values stay closer to the root chord. 730 nir_strength: Melodic inertia (0.0 to 1.0). Influences chord movement 731 expectations. 732 minor_turnaround_weight: For "turnaround" style, influences major vs minor feel. 733 root_diversity: Root-repetition damping (0.0 to 1.0). Each recent 734 chord sharing a candidate's root reduces the weight to 40% at 735 the default (0.4). Set to 1.0 to disable. 736 reschedule_lookahead: How many beats in advance to calculate the 737 next chord. 738 739 Example: 740 ```python 741 # A moody minor progression that changes every 8 beats 742 comp.harmony(style="aeolian_minor", cycle_beats=8, gravity=0.4) 743 ``` 744 """ 745 746 if self.key is None: 747 raise ValueError("Cannot configure harmony without a key - set key in the Composition constructor") 748 749 preserved_history: typing.List[subsequence.chords.Chord] = [] 750 preserved_current: typing.Optional[subsequence.chords.Chord] = None 751 752 if self._harmonic_state is not None: 753 preserved_history = self._harmonic_state.history.copy() 754 preserved_current = self._harmonic_state.current_chord 755 756 self._harmonic_state = subsequence.harmonic_state.HarmonicState( 757 key_name = self.key, 758 graph_style = style, 759 include_dominant_7th = dominant_7th, 760 key_gravity_blend = gravity, 761 nir_strength = nir_strength, 762 minor_turnaround_weight = minor_turnaround_weight, 763 root_diversity = root_diversity 764 ) 765 766 if preserved_history: 767 self._harmonic_state.history = preserved_history 768 if preserved_current is not None and self._harmonic_state.graph.get_transitions(preserved_current): 769 self._harmonic_state.current_chord = preserved_current 770 771 self._harmony_cycle_beats = cycle_beats 772 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)
774 def freeze (self, bars: int) -> "Progression": 775 776 """Capture a chord progression from the live harmony engine. 777 778 Runs the harmony engine forward by *bars* chord changes, records each 779 chord, and returns it as a :class:`Progression` that can be bound to a 780 form section with :meth:`section_chords`. 781 782 The engine state **advances** — successive ``freeze()`` calls produce a 783 continuing compositional journey so section progressions feel like parts 784 of a whole rather than isolated islands. 785 786 Parameters: 787 bars: Number of chords to capture (one per harmony cycle). 788 789 Returns: 790 A :class:`Progression` with the captured chords and trailing 791 history for NIR continuity. 792 793 Raises: 794 ValueError: If :meth:`harmony` has not been called first. 795 796 Example:: 797 798 composition.harmony(style="functional_major", cycle_beats=4) 799 verse = composition.freeze(8) # 8 chords, engine advances 800 chorus = composition.freeze(4) # next 4 chords, continuing on 801 composition.section_chords("verse", verse) 802 composition.section_chords("chorus", chorus) 803 """ 804 805 hs = self._require_harmonic_state() 806 807 if bars < 1: 808 raise ValueError("bars must be at least 1") 809 collected: typing.List[subsequence.chords.Chord] = [hs.current_chord] 810 811 for _ in range(bars - 1): 812 hs.step() 813 collected.append(hs.current_chord) 814 815 # Advance past the last captured chord so the next freeze() call or 816 # live playback does not duplicate it. 817 hs.step() 818 819 return Progression( 820 chords = tuple(collected), 821 trailing_history = tuple(hs.history), 822 )
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)
824 def section_chords (self, section_name: str, progression: "Progression") -> None: 825 826 """Bind a frozen :class:`Progression` to a named form section. 827 828 Every time *section_name* plays, the harmonic clock replays the 829 progression's chords in order instead of calling the live engine. 830 Sections without a bound progression continue generating live chords. 831 832 Parameters: 833 section_name: Name of the section as defined in :meth:`form`. 834 progression: The :class:`Progression` returned by :meth:`freeze`. 835 836 Raises: 837 ValueError: If the form has been configured and *section_name* is 838 not a known section name. 839 840 Example:: 841 842 composition.section_chords("verse", verse_progression) 843 composition.section_chords("chorus", chorus_progression) 844 # "bridge" is not bound — it generates live chords 845 """ 846 847 if ( 848 self._form_state is not None 849 and self._form_state._section_bars is not None 850 and section_name not in self._form_state._section_bars 851 ): 852 known = ", ".join(sorted(self._form_state._section_bars)) 853 raise ValueError( 854 f"Section '{section_name}' not found in form. " 855 f"Known sections: {known}" 856 ) 857 858 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
860 def on_event (self, event_name: str, callback: typing.Callable[..., typing.Any]) -> None: 861 862 """ 863 Register a callback for a sequencer event (e.g., "bar", "start", "stop"). 864 """ 865 866 self._sequencer.on_event(event_name, callback)
Register a callback for a sequencer event (e.g., "bar", "start", "stop").
873 def hotkeys (self, enabled: bool = True) -> None: 874 875 """Enable or disable the global hotkey listener. 876 877 Must be called **before** :meth:`play` to take effect. When enabled, a 878 background thread reads single keystrokes from stdin without requiring 879 Enter. The ``?`` key is always reserved and lists all active bindings. 880 881 Hotkeys have zero impact on playback when disabled — the listener 882 thread is never started. 883 884 Args: 885 enabled: ``True`` (default) to enable hotkeys; ``False`` to disable. 886 887 Example:: 888 889 composition.hotkeys() 890 composition.hotkey("a", lambda: composition.form_jump("chorus")) 891 composition.play() 892 """ 893 894 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()
897 def hotkey ( 898 self, 899 key: str, 900 action: typing.Callable[[], None], 901 quantize: int = 0, 902 label: typing.Optional[str] = None, 903 ) -> None: 904 905 """Register a single-key shortcut that fires during playback. 906 907 The listener must be enabled first with :meth:`hotkeys`. 908 909 Most actions — form jumps, ``composition.data`` writes, and 910 :meth:`tweak` calls — should use ``quantize=0`` (the default). Their 911 musical effect is naturally delayed to the next pattern rebuild cycle, 912 which provides automatic musical quantization without extra configuration. 913 914 Use ``quantize=N`` for actions where you want an explicit bar-boundary 915 guarantee, such as :meth:`mute` / :meth:`unmute`. 916 917 The ``?`` key is reserved and cannot be overridden. 918 919 Args: 920 key: A single character trigger (e.g. ``"a"``, ``"1"``, ``" "``). 921 action: Zero-argument callable to execute. 922 quantize: ``0`` = execute immediately (default). ``N`` = execute 923 on the next global bar number divisible by *N*. 924 label: Display name for the ``?`` help listing. Auto-derived from 925 the function name or lambda body if omitted. 926 927 Raises: 928 ValueError: If ``key`` is the reserved ``?`` character, or if 929 ``key`` is not exactly one character. 930 931 Example:: 932 933 composition.hotkeys() 934 935 # Immediate — musical effect happens at next pattern rebuild 936 composition.hotkey("a", lambda: composition.form_jump("chorus")) 937 composition.hotkey("1", lambda: composition.data.update({"mode": "chill"})) 938 939 # Explicit 4-bar phrase boundary 940 composition.hotkey("s", lambda: composition.mute("drums"), quantize=4) 941 942 # Named function — label is derived automatically 943 def drop_to_breakdown(): 944 composition.form_jump("breakdown") 945 composition.mute("lead") 946 947 composition.hotkey("d", drop_to_breakdown) 948 949 composition.play() 950 """ 951 952 if len(key) != 1: 953 raise ValueError(f"hotkey key must be a single character, got {key!r}") 954 955 if key == _HOTKEY_RESERVED: 956 raise ValueError(f"'{_HOTKEY_RESERVED}' is reserved for listing active hotkeys.") 957 958 derived = label if label is not None else _derive_label(action) 959 960 self._hotkey_bindings[key] = HotkeyBinding( 961 key = key, 962 action = action, 963 quantize = quantize, 964 label = derived, 965 )
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()
968 def form_jump (self, section_name: str) -> None: 969 970 """Jump the form to a named section immediately. 971 972 Delegates to :meth:`subsequence.form_state.FormState.jump_to`. Only works when the 973 composition uses graph-mode form (a dict passed to :meth:`form`). 974 975 The musical effect is heard at the *next pattern rebuild cycle* — already- 976 queued MIDI notes are unaffected. This natural delay means ``form_jump`` 977 is effective without needing explicit quantization. 978 979 Args: 980 section_name: The section to jump to. 981 982 Raises: 983 ValueError: If no form is configured, or the form is not in graph 984 mode, or *section_name* is unknown. 985 986 Example:: 987 988 composition.hotkey("c", lambda: composition.form_jump("chorus")) 989 """ 990 991 if self._form_state is None: 992 raise ValueError("form_jump() requires a form to be configured via composition.form().") 993 994 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"))
997 def form_next (self, section_name: str) -> None: 998 999 """Queue the next section — takes effect when the current section ends. 1000 1001 Unlike :meth:`form_jump`, this does not interrupt the current section. 1002 The queued section replaces the automatically pre-decided next section 1003 and takes effect at the natural section boundary. The performer can 1004 change their mind by calling ``form_next`` again before the boundary. 1005 1006 Delegates to :meth:`subsequence.form_state.FormState.queue_next`. Only works when the 1007 composition uses graph-mode form (a dict passed to :meth:`form`). 1008 1009 Args: 1010 section_name: The section to queue. 1011 1012 Raises: 1013 ValueError: If no form is configured, or the form is not in graph 1014 mode, or *section_name* is unknown. 1015 1016 Example:: 1017 1018 composition.hotkey("c", lambda: composition.form_next("chorus")) 1019 """ 1020 1021 if self._form_state is None: 1022 raise ValueError("form_next() requires a form to be configured via composition.form().") 1023 1024 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"))
1107 def seed (self, value: int) -> None: 1108 1109 """ 1110 Set a random seed for deterministic, repeatable playback. 1111 1112 If a seed is set, Subsequence will produce the exact same sequence 1113 every time you run the script. This is vital for finishing tracks or 1114 reproducing a specific 'performance'. 1115 1116 Parameters: 1117 value: An integer seed. 1118 1119 Example: 1120 ```python 1121 # Fix the randomness 1122 comp.seed(42) 1123 ``` 1124 """ 1125 1126 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)
1128 def display (self, enabled: bool = True, grid: bool = False, grid_scale: float = 1.0) -> None: 1129 1130 """ 1131 Enable or disable the live terminal dashboard. 1132 1133 When enabled, Subsequence uses a safe logging handler that allows a 1134 persistent status line (BPM, Key, Bar, Section, Chord) to stay at 1135 the bottom of the terminal while logs scroll above it. 1136 1137 Parameters: 1138 enabled: Whether to show the display (default True). 1139 grid: When True, render an ASCII grid visualisation of all 1140 running patterns above the status line. The grid updates 1141 once per bar, showing which steps have notes and at what 1142 velocity. 1143 grid_scale: Horizontal zoom factor for the grid (default 1144 ``1.0``). Higher values add visual columns between 1145 grid steps, revealing micro-timing from swing and groove. 1146 Snapped to the nearest integer internally for uniform 1147 marker spacing. 1148 """ 1149 1150 if enabled: 1151 self._display = subsequence.display.Display(self, grid=grid, grid_scale=grid_scale) 1152 else: 1153 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.
1155 def web_ui (self) -> None: 1156 1157 """ 1158 Enable the realtime Web UI Dashboard. 1159 1160 When enabled, Subsequence instantiates a WebSocket server that broadcasts 1161 the current state, signals, and active patterns (with high-res timing and note data) 1162 to any connected browser clients. 1163 """ 1164 1165 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.
1167 def midi_input (self, device: str, clock_follow: bool = False) -> None: 1168 1169 """ 1170 Configure MIDI input for external sync and MIDI messages. 1171 1172 Parameters: 1173 device: The name of the MIDI input port. 1174 clock_follow: If True, Subsequence will slave its clock to incoming 1175 MIDI Ticks. It will also follow MIDI Start/Stop/Continue 1176 commands. 1177 1178 Example: 1179 ```python 1180 # Slave Subsequence to an external hardware sequencer 1181 comp.midi_input("Scarlett 2i4", clock_follow=True) 1182 ``` 1183 """ 1184 1185 self._input_device = device 1186 self._clock_follow = clock_follow
Configure MIDI input for external sync and MIDI messages.
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.
Example:
# Slave Subsequence to an external hardware sequencer comp.midi_input("Scarlett 2i4", clock_follow=True)
1188 def clock_output (self, enabled: bool = True) -> None: 1189 1190 """ 1191 Send MIDI timing clock to connected hardware. 1192 1193 When enabled, Subsequence acts as a MIDI clock master and sends 1194 standard clock messages on the output port: a Start message (0xFA) 1195 when playback begins, a Clock tick (0xF8) on every pulse (24 PPQN), 1196 and a Stop message (0xFC) when playback ends. 1197 1198 This allows hardware synthesizers, drum machines, and effect units to 1199 slave their tempo to Subsequence automatically. 1200 1201 **Note:** Clock output is automatically disabled when ``midi_input()`` 1202 is called with ``clock_follow=True``, to prevent a clock feedback loop. 1203 1204 Parameters: 1205 enabled: Whether to send MIDI clock (default True). 1206 1207 Example: 1208 ```python 1209 comp = subsequence.Composition(bpm=120, output_device="...") 1210 comp.clock_output() # hardware will follow Subsequence tempo 1211 ``` 1212 """ 1213 1214 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
1217 def link (self, quantum: float = 4.0) -> "Composition": 1218 1219 """ 1220 Enable Ableton Link tempo and phase synchronisation. 1221 1222 When enabled, Subsequence joins the local Link session and slaves its 1223 clock to the shared network tempo and beat phase. All other Link-enabled 1224 apps on the same LAN — Ableton Live, iOS synths, other Subsequence 1225 instances — will automatically stay in time. 1226 1227 Playback starts on the next bar boundary aligned to the Link quantum, 1228 so downbeats stay in sync across all participants. 1229 1230 Requires the ``link`` optional extra:: 1231 1232 pip install subsequence[link] 1233 1234 Parameters: 1235 quantum: Beat cycle length. ``4.0`` (default) = one bar in 4/4 time. 1236 Change this if your composition uses a different meter. 1237 1238 Example:: 1239 1240 comp = subsequence.Composition(bpm=120, key="C") 1241 comp.link() # join the Link session 1242 comp.play() 1243 1244 # On another machine / instance: 1245 comp2 = subsequence.Composition(bpm=120) 1246 comp2.link() # tempo and phase will lock to comp 1247 comp2.play() 1248 1249 Note: 1250 ``set_bpm()`` proposes the new tempo to the Link network when Link 1251 is active. The network-authoritative tempo is applied on the next 1252 pulse, so there may be a brief lag before the change is visible. 1253 """ 1254 1255 # Eagerly check that aalink is installed — fail early with a clear message. 1256 subsequence.link_clock._require_aalink() 1257 1258 self._link_quantum = quantum 1259 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.
1262 def cc_map ( 1263 self, 1264 cc: int, 1265 key: str, 1266 channel: typing.Optional[int] = None, 1267 min_val: float = 0.0, 1268 max_val: float = 1.0 1269 ) -> None: 1270 1271 """ 1272 Map an incoming MIDI CC to a ``composition.data`` key. 1273 1274 When the composition receives a CC message on the configured MIDI 1275 input port, the value is scaled from the CC range (0–127) to 1276 *[min_val, max_val]* and stored in ``composition.data[key]``. 1277 1278 This lets hardware knobs, faders, and expression pedals control live 1279 parameters without writing any callback code. 1280 1281 **Requires** ``midi_input()`` to be called first to open an input port. 1282 1283 Parameters: 1284 cc: MIDI Control Change number (0–127). 1285 key: The ``composition.data`` key to write. 1286 channel: If given, only respond to CC messages on this channel. 1287 Uses the same numbering convention as ``pattern()`` (0-15 1288 by default, or 1-16 with ``zero_indexed_channels=False``). 1289 ``None`` matches any channel (default). 1290 min_val: Scaled minimum — written when CC value is 0 (default 0.0). 1291 max_val: Scaled maximum — written when CC value is 127 (default 1.0). 1292 1293 Example: 1294 ```python 1295 comp.midi_input("Arturia KeyStep") 1296 comp.cc_map(74, "filter_cutoff") # knob → 0.0–1.0 1297 comp.cc_map(7, "volume", min_val=0, max_val=127) # volume fader 1298 ``` 1299 """ 1300 1301 resolved_channel = self._resolve_channel(channel) if channel is not None else None 1302 1303 self._cc_mappings.append({ 1304 'cc': cc, 1305 'key': key, 1306 'channel': resolved_channel, 1307 'min_val': min_val, 1308 'max_val': max_val, 1309 })
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).
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
1312 def live (self, port: int = 5555) -> None: 1313 1314 """ 1315 Enable the live coding eval server. 1316 1317 This allows you to connect to a running composition using the 1318 `subsequence.live_client` REPL and hot-swap pattern code or 1319 modify variables in real-time. 1320 1321 Parameters: 1322 port: The TCP port to listen on (default 5555). 1323 """ 1324 1325 self._live_server = subsequence.live_server.LiveServer(self, port=port) 1326 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).
1328 def osc (self, receive_port: int = 9000, send_port: int = 9001, send_host: str = "127.0.0.1") -> None: 1329 1330 """ 1331 Enable bi-directional Open Sound Control (OSC). 1332 1333 Subsequence will listen for commands (like `/bpm` or `/mute`) and 1334 broadcast its internal state (like `/chord` or `/bar`) over UDP. 1335 1336 Parameters: 1337 receive_port: Port to listen for incoming OSC messages (default 9000). 1338 send_port: Port to send state updates to (default 9001). 1339 send_host: The IP address to send updates to (default "127.0.0.1"). 1340 """ 1341 1342 self._osc_server = subsequence.osc.OscServer( 1343 self, 1344 receive_port = receive_port, 1345 send_port = send_port, 1346 send_host = send_host 1347 )
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").
1349 def set_bpm (self, bpm: float) -> None: 1350 1351 """ 1352 Instantly change the tempo. 1353 1354 Parameters: 1355 bpm: The new tempo in beats per minute. 1356 1357 When Ableton Link is active, this proposes the new tempo to the Link 1358 network instead of applying it locally. The network-authoritative tempo 1359 is picked up on the next pulse. 1360 """ 1361 1362 self._sequencer.set_bpm(bpm) 1363 1364 if not self._clock_follow and self._link_quantum is None: 1365 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.
1367 def target_bpm (self, bpm: float, bars: int, shape: str = "linear") -> None: 1368 1369 """ 1370 Smoothly ramp the tempo to a target value over a number of bars. 1371 1372 Parameters: 1373 bpm: Target tempo in beats per minute. 1374 bars: Duration of the transition in bars. 1375 shape: Easing curve name. Defaults to ``"linear"``. 1376 ``"ease_in_out"`` or ``"s_curve"`` are recommended for natural- 1377 sounding tempo changes. See :mod:`subsequence.easing` for all 1378 available shapes. 1379 1380 Example: 1381 ```python 1382 # Accelerate to 140 BPM over the next 8 bars with a smooth S-curve 1383 comp.target_bpm(140, bars=8, shape="ease_in_out") 1384 ``` 1385 """ 1386 1387 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")
1389 def live_info (self) -> typing.Dict[str, typing.Any]: 1390 1391 """ 1392 Return a dictionary containing the current state of the composition. 1393 1394 Includes BPM, key, current bar, active section, current chord, 1395 running patterns, and custom data. 1396 """ 1397 1398 section_info = None 1399 if self._form_state is not None: 1400 section = self._form_state.get_section_info() 1401 if section is not None: 1402 section_info = { 1403 "name": section.name, 1404 "bar": section.bar, 1405 "bars": section.bars, 1406 "progress": section.progress 1407 } 1408 1409 chord_name = None 1410 if self._harmonic_state is not None: 1411 chord = self._harmonic_state.get_current_chord() 1412 if chord is not None: 1413 chord_name = chord.name() 1414 1415 pattern_list = [] 1416 channel_offset = 0 if self._zero_indexed_channels else 1 1417 for name, pat in self._running_patterns.items(): 1418 pattern_list.append({ 1419 "name": name, 1420 "channel": pat.channel + channel_offset, 1421 "length": pat.length, 1422 "cycle": pat._cycle_count, 1423 "muted": pat._muted, 1424 "tweaks": dict(pat._tweaks) 1425 }) 1426 1427 return { 1428 "bpm": self._sequencer.current_bpm, 1429 "key": self.key, 1430 "bar": self._builder_bar, 1431 "section": section_info, 1432 "chord": chord_name, 1433 "patterns": pattern_list, 1434 "input_device": self._input_device, 1435 "clock_follow": self._clock_follow, 1436 "data": self.data 1437 }
Return a dictionary containing the current state of the composition.
Includes BPM, key, current bar, active section, current chord, running patterns, and custom data.
1439 def mute (self, name: str) -> None: 1440 1441 """ 1442 Mute a running pattern by name. 1443 1444 The pattern continues to 'run' and increment its cycle count in 1445 the background, but it will not produce any MIDI notes until unmuted. 1446 1447 Parameters: 1448 name: The function name of the pattern to mute. 1449 """ 1450 1451 if name not in self._running_patterns: 1452 raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}") 1453 1454 self._running_patterns[name]._muted = True 1455 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.
1457 def unmute (self, name: str) -> None: 1458 1459 """ 1460 Unmute a previously muted pattern. 1461 """ 1462 1463 if name not in self._running_patterns: 1464 raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}") 1465 1466 self._running_patterns[name]._muted = False 1467 logger.info(f"Unmuted pattern: {name}")
Unmute a previously muted pattern.
1469 def tweak (self, name: str, **kwargs: typing.Any) -> None: 1470 1471 """Override parameters for a running pattern. 1472 1473 Values set here are available inside the pattern's builder 1474 function via ``p.param()``. They persist across rebuilds 1475 until explicitly changed or cleared. Changes take effect 1476 on the next rebuild cycle. 1477 1478 Parameters: 1479 name: The function name of the pattern. 1480 **kwargs: Parameter names and their new values. 1481 1482 Example (from the live REPL):: 1483 1484 composition.tweak("bass", pitches=[48, 52, 55, 60]) 1485 """ 1486 1487 if name not in self._running_patterns: 1488 raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}") 1489 1490 self._running_patterns[name]._tweaks.update(kwargs) 1491 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])
1493 def clear_tweak (self, name: str, *param_names: str) -> None: 1494 1495 """Remove tweaked parameters from a running pattern. 1496 1497 If no parameter names are given, all tweaks for the pattern 1498 are cleared and every ``p.param()`` call reverts to its 1499 default. 1500 1501 Parameters: 1502 name: The function name of the pattern. 1503 *param_names: Specific parameter names to clear. If 1504 omitted, all tweaks are removed. 1505 """ 1506 1507 if name not in self._running_patterns: 1508 raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}") 1509 1510 if not param_names: 1511 self._running_patterns[name]._tweaks.clear() 1512 logger.info(f"Cleared all tweaks for pattern '{name}'") 1513 else: 1514 for param_name in param_names: 1515 self._running_patterns[name]._tweaks.pop(param_name, None) 1516 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.
1518 def get_tweaks (self, name: str) -> typing.Dict[str, typing.Any]: 1519 1520 """Return a copy of the current tweaks for a running pattern. 1521 1522 Parameters: 1523 name: The function name of the pattern. 1524 """ 1525 1526 if name not in self._running_patterns: 1527 raise ValueError(f"Pattern '{name}' not found. Available: {list(self._running_patterns.keys())}") 1528 1529 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.
1531 def schedule (self, fn: typing.Callable, cycle_beats: int, reschedule_lookahead: int = 1, wait_for_initial: bool = False, defer: bool = False) -> None: 1532 1533 """ 1534 Register a custom function to run on a repeating beat-based cycle. 1535 1536 Subsequence automatically runs synchronous functions in a thread pool 1537 so they don't block the timing-critical MIDI clock. Async functions 1538 are run directly on the event loop. 1539 1540 Parameters: 1541 fn: The function to call. 1542 cycle_beats: How often to call it (e.g., 4 = every bar). 1543 reschedule_lookahead: How far in advance to schedule the next call. 1544 wait_for_initial: If True, run the function once during startup 1545 and wait for it to complete before playback begins. This 1546 ensures ``composition.data`` is populated before patterns 1547 first build. Implies ``defer=True`` for the repeating 1548 schedule. 1549 defer: If True, skip the pulse-0 fire and defer the first 1550 repeating call to just before the second cycle boundary. 1551 """ 1552 1553 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.
1555 def form ( 1556 self, 1557 sections: typing.Union[ 1558 typing.List[typing.Tuple[str, int]], 1559 typing.Iterator[typing.Tuple[str, int]], 1560 typing.Dict[str, typing.Tuple[int, typing.Optional[typing.List[typing.Tuple[str, int]]]]] 1561 ], 1562 loop: bool = False, 1563 start: typing.Optional[str] = None 1564 ) -> None: 1565 1566 """ 1567 Define the structure (sections) of the composition. 1568 1569 You can define form in three ways: 1570 1. **Graph (Dict)**: Dynamic transitions based on weights. 1571 2. **Sequence (List)**: A fixed order of sections. 1572 3. **Generator**: A Python generator that yields `(name, bars)` pairs. 1573 1574 Parameters: 1575 sections: The form definition (Dict, List, or Generator). 1576 loop: Whether to cycle back to the start (List mode only). 1577 start: The section to start with (Graph mode only). 1578 1579 Example: 1580 ```python 1581 # A simple pop structure 1582 comp.form([ 1583 ("verse", 8), 1584 ("chorus", 8), 1585 ("verse", 8), 1586 ("chorus", 16) 1587 ]) 1588 ``` 1589 """ 1590 1591 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) ])
1593 def pattern ( 1594 self, 1595 channel: int, 1596 length: float = 4, 1597 unit: typing.Optional[float] = None, 1598 drum_note_map: typing.Optional[typing.Dict[str, int]] = None, 1599 reschedule_lookahead: float = 1, 1600 voice_leading: bool = False 1601 ) -> typing.Callable: 1602 1603 """ 1604 Register a function as a repeating MIDI pattern. 1605 1606 The decorated function will be called once per cycle to 'rebuild' its 1607 content. This allows for generative logic that evolves over time. 1608 1609 When ``unit`` is provided, ``length`` is a note count and the actual 1610 duration in beats is ``length * unit``. The note count also becomes 1611 the default grid size for ``hit_steps()`` and ``sequence()``. 1612 1613 When ``unit`` is omitted, ``length`` is in beats (quarter notes) and 1614 the grid defaults to sixteenth-note resolution. 1615 1616 Parameters: 1617 channel: MIDI channel. By default uses 0-based numbering (0-15) 1618 matching the raw MIDI protocol. Set 1619 ``zero_indexed_channels=False`` on the ``Composition`` to use 1620 1-based numbering (1-16) instead. 1621 length: Note count when ``unit`` is given, otherwise duration 1622 in beats (default 4). 1623 unit: Duration of one note in beats (e.g. ``dur.SIXTEENTH``). 1624 When set, ``length`` is treated as a count and the grid 1625 defaults to ``length``. 1626 drum_note_map: Optional mapping for drum instruments. 1627 reschedule_lookahead: Beats in advance to compute the next cycle. 1628 voice_leading: If True, chords in this pattern will automatically 1629 use inversions that minimize voice movement. 1630 1631 Example: 1632 ```python 1633 @comp.pattern(channel=1, length=6, unit=dur.SIXTEENTH) 1634 def riff(p): 1635 p.sequence(steps=[0, 1, 3, 5], pitches=60) 1636 ``` 1637 """ 1638 1639 channel = self._resolve_channel(channel) 1640 1641 if unit is not None: 1642 beat_length = length * unit 1643 default_grid = int(length) 1644 else: 1645 beat_length = length 1646 default_grid = round(beat_length / subsequence.constants.durations.SIXTEENTH) 1647 1648 def decorator (fn: typing.Callable) -> typing.Callable: 1649 1650 """ 1651 Wrap the builder function and register it as a pending pattern. 1652 During live sessions, hot-swap an existing pattern's builder instead. 1653 """ 1654 1655 # Hot-swap: if we're live and a pattern with this name exists, replace its builder. 1656 if self._is_live and fn.__name__ in self._running_patterns: 1657 running = self._running_patterns[fn.__name__] 1658 running._builder_fn = fn 1659 running._wants_chord = _fn_has_parameter(fn, "chord") 1660 logger.info(f"Hot-swapped pattern: {fn.__name__}") 1661 return fn 1662 1663 pending = _PendingPattern( 1664 builder_fn = fn, 1665 channel = channel, # already resolved to 0-indexed 1666 length = beat_length, 1667 default_grid = default_grid, 1668 drum_note_map = drum_note_map, 1669 reschedule_lookahead = reschedule_lookahead, 1670 voice_leading = voice_leading 1671 ) 1672 1673 self._pending_patterns.append(pending) 1674 1675 return fn 1676 1677 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.
When unit is provided, length is a note count and the actual
duration in beats is length * unit. The note count also becomes
the default grid size for hit_steps() and sequence().
When unit is omitted, length is in beats (quarter notes) and
the grid defaults to sixteenth-note resolution.
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. - length: Note count when
unitis given, otherwise duration in beats (default 4). - unit: Duration of one note in beats (e.g.
dur.SIXTEENTH). When set,lengthis treated as a count and the grid defaults tolength. - 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, length=6, unit=dur.SIXTEENTH) def riff(p): p.sequence(steps=[0, 1, 3, 5], pitches=60)
1679 def layer ( 1680 self, 1681 *builder_fns: typing.Callable, 1682 channel: int, 1683 length: float = 4, 1684 unit: typing.Optional[float] = None, 1685 drum_note_map: typing.Optional[typing.Dict[str, int]] = None, 1686 reschedule_lookahead: float = 1, 1687 voice_leading: bool = False 1688 ) -> None: 1689 1690 """ 1691 Combine multiple functions into a single MIDI pattern. 1692 1693 This is useful for composing complex patterns out of reusable 1694 building blocks (e.g., a 'kick' function and a 'snare' function). 1695 1696 Parameters: 1697 builder_fns: One or more pattern builder functions. 1698 channel: MIDI channel (0-15, or 1-16 with ``zero_indexed_channels=False``). 1699 length: Note count when ``unit`` is given, otherwise duration 1700 in beats (default 4). 1701 unit: Duration of one note in beats (e.g. ``dur.SIXTEENTH``). 1702 When set, ``length`` is treated as a count and the grid 1703 defaults to ``length``. 1704 drum_note_map: Optional mapping for drum instruments. 1705 reschedule_lookahead: Beats in advance to compute the next cycle. 1706 voice_leading: If True, chords use smooth voice leading. 1707 """ 1708 1709 if unit is not None: 1710 beat_length = length * unit 1711 default_grid = int(length) 1712 else: 1713 beat_length = length 1714 default_grid = round(beat_length / subsequence.constants.durations.SIXTEENTH) 1715 1716 wants_chord = any(_fn_has_parameter(fn, "chord") for fn in builder_fns) 1717 1718 if wants_chord: 1719 1720 def merged_builder (p: subsequence.pattern_builder.PatternBuilder, chord: _InjectedChord) -> None: 1721 1722 for fn in builder_fns: 1723 if _fn_has_parameter(fn, "chord"): 1724 fn(p, chord) 1725 else: 1726 fn(p) 1727 1728 else: 1729 1730 def merged_builder (p: subsequence.pattern_builder.PatternBuilder) -> None: # type: ignore[misc] 1731 1732 for fn in builder_fns: 1733 fn(p) 1734 1735 resolved = self._resolve_channel(channel) 1736 1737 pending = _PendingPattern( 1738 builder_fn = merged_builder, 1739 channel = resolved, # already resolved to 0-indexed 1740 length = beat_length, 1741 default_grid = default_grid, 1742 drum_note_map = drum_note_map, 1743 reschedule_lookahead = reschedule_lookahead, 1744 voice_leading = voice_leading 1745 ) 1746 1747 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).
Arguments:
- builder_fns: One or more pattern builder functions.
- channel: MIDI channel (0-15, or 1-16 with
zero_indexed_channels=False). - length: Note count when
unitis given, otherwise duration in beats (default 4). - unit: Duration of one note in beats (e.g.
dur.SIXTEENTH). When set,lengthis treated as a count and the grid defaults tolength. - 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.
1749 def trigger ( 1750 self, 1751 fn: typing.Callable, 1752 channel: int, 1753 length: float = 1, 1754 quantize: float = 0, 1755 drum_note_map: typing.Optional[typing.Dict[str, int]] = None, 1756 chord: bool = False 1757 ) -> None: 1758 1759 """ 1760 Trigger a one-shot pattern immediately or on a quantized boundary. 1761 1762 This is useful for real-time response to sensors, OSC messages, or other 1763 external events. The builder function is called immediately with a fresh 1764 PatternBuilder, and the generated events are injected into the queue at 1765 the specified quantize boundary. 1766 1767 The builder function has the same API as a ``@composition.pattern`` 1768 decorated function and can use all PatternBuilder methods: ``p.note()``, 1769 ``p.euclidean()``, ``p.arpeggio()``, and so on. 1770 1771 Parameters: 1772 fn: The pattern builder function (same signature as ``@comp.pattern``). 1773 channel: MIDI channel (0-15, or 1-16 with ``zero_indexed_channels=False``). 1774 length: Duration in beats (default 1). This is the time window for the 1775 one-shot pattern. 1776 quantize: Snap the trigger to a beat boundary: ``0`` = immediate (default), 1777 ``1`` = next beat (quarter note), ``4`` = next bar. Use ``dur.*`` 1778 constants from ``subsequence.constants.durations``. 1779 drum_note_map: Optional drum name mapping for this pattern. 1780 chord: If ``True``, the builder function receives the current chord as 1781 a second parameter (same as ``@composition.pattern``). 1782 1783 Example: 1784 ```python 1785 # Immediate single note 1786 composition.trigger( 1787 lambda p: p.note(60, beat=0, velocity=100, duration=0.5), 1788 channel=0 1789 ) 1790 1791 # Quantized fill (next bar) 1792 import subsequence.constants.durations as dur 1793 composition.trigger( 1794 lambda p: p.euclidean("snare", pulses=7, velocity=90), 1795 channel=9, 1796 drum_note_map=gm_drums.GM_DRUM_MAP, 1797 quantize=dur.WHOLE 1798 ) 1799 1800 # With chord context 1801 composition.trigger( 1802 lambda p: p.arpeggio(p.chord.tones(root=60), step=dur.SIXTEENTH), 1803 channel=0, 1804 quantize=dur.QUARTER, 1805 chord=True 1806 ) 1807 ``` 1808 """ 1809 1810 # Resolve channel numbering 1811 resolved_channel = self._resolve_channel(channel) 1812 1813 # Create a temporary Pattern 1814 pattern = subsequence.pattern.Pattern(channel=resolved_channel, length=length) 1815 1816 # Create a PatternBuilder 1817 builder = subsequence.pattern_builder.PatternBuilder( 1818 pattern=pattern, 1819 cycle=0, # One-shot patterns don't rebuild, so cycle is always 0 1820 drum_note_map=drum_note_map, 1821 section=self._form_state.get_section_info() if self._form_state else None, 1822 bar=self._builder_bar, 1823 conductor=self.conductor, 1824 rng=random.Random(), # Fresh random state for each trigger 1825 tweaks={}, 1826 default_grid=round(length / subsequence.constants.durations.SIXTEENTH), 1827 data=self.data 1828 ) 1829 1830 # Call the builder function 1831 try: 1832 1833 if chord and self._harmonic_state is not None: 1834 current_chord = self._harmonic_state.get_current_chord() 1835 injected = _InjectedChord(current_chord, None) # No voice leading for one-shots 1836 fn(builder, injected) 1837 1838 else: 1839 fn(builder) 1840 1841 except Exception: 1842 logger.exception("Error in trigger builder — pattern will be silent") 1843 return 1844 1845 # Calculate the start pulse based on quantize 1846 current_pulse = self._sequencer.pulse_count 1847 pulses_per_beat = subsequence.constants.MIDI_QUARTER_NOTE 1848 1849 if quantize == 0: 1850 # Immediate: use current pulse 1851 start_pulse = current_pulse 1852 1853 else: 1854 # Quantize to the next multiple of (quantize * pulses_per_beat) 1855 quantize_pulses = int(quantize * pulses_per_beat) 1856 start_pulse = ((current_pulse // quantize_pulses) + 1) * quantize_pulses 1857 1858 # Schedule the pattern for one-shot execution 1859 try: 1860 loop = asyncio.get_running_loop() 1861 # Already on the event loop 1862 asyncio.create_task(self._sequencer.schedule_pattern(pattern, start_pulse)) 1863 1864 except RuntimeError: 1865 # Not on the event loop — schedule via call_soon_threadsafe 1866 if self._sequencer._event_loop is not None: 1867 asyncio.run_coroutine_threadsafe( 1868 self._sequencer.schedule_pattern(pattern, start_pulse), 1869 loop=self._sequencer._event_loop 1870 ) 1871 else: 1872 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.
Arguments:
- fn: The pattern builder function (same signature as
@comp.pattern). - channel: MIDI channel (0-15, or 1-16 with
zero_indexed_channels=False). - length: Duration in beats (default 1). This is the time window for the one-shot pattern.
- 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), step=dur.SIXTEENTH), channel=0, quantize=dur.QUARTER, chord=True )
1874 def play (self) -> None: 1875 1876 """ 1877 Start the composition. 1878 1879 This call blocks until the program is interrupted (e.g., via Ctrl+C). 1880 It initializes the MIDI hardware, launches the background sequencer, 1881 and begins playback. 1882 """ 1883 1884 try: 1885 asyncio.run(self._run()) 1886 1887 except KeyboardInterrupt: 1888 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.
1891 def render (self, bars: typing.Optional[int] = None, filename: str = "render.mid", max_minutes: typing.Optional[float] = 60.0) -> None: 1892 1893 """Render the composition to a MIDI file without real-time playback. 1894 1895 Runs the sequencer as fast as possible (no timing delays) and stops 1896 when the first active limit is reached. The result is saved as a 1897 standard MIDI file that can be imported into any DAW. 1898 1899 All patterns, scheduled callbacks, and harmony logic run exactly as 1900 they would during live playback — BPM transitions, generative fills, 1901 and probabilistic gates all work in render mode. The only difference 1902 is that time is simulated rather than wall-clock driven. 1903 1904 Parameters: 1905 bars: Number of bars to render, or ``None`` for no bar limit 1906 (default ``None``). When both *bars* and *max_minutes* are 1907 active, playback stops at whichever limit is reached first. 1908 filename: Output MIDI filename (default ``"render.mid"``). 1909 max_minutes: Safety cap on the length of rendered MIDI in minutes 1910 (default ``60.0``). Pass ``None`` to disable the time 1911 cap — you must then provide an explicit *bars* value. 1912 1913 Raises: 1914 ValueError: If both *bars* and *max_minutes* are ``None``, which 1915 would produce an infinite render. 1916 1917 Examples: 1918 ```python 1919 # Default: renders up to 60 minutes of MIDI content. 1920 composition.render() 1921 1922 # Render exactly 64 bars (time cap still active as backstop). 1923 composition.render(bars=64, filename="demo.mid") 1924 1925 # Render up to 5 minutes of an infinite generative composition. 1926 composition.render(max_minutes=5, filename="five_min.mid") 1927 1928 # Remove the time cap — must supply bars instead. 1929 composition.render(bars=128, max_minutes=None, filename="long.mid") 1930 ``` 1931 """ 1932 1933 if bars is None and max_minutes is None: 1934 raise ValueError( 1935 "render() requires at least one limit: provide bars=, max_minutes=, or both. " 1936 "Passing both as None would produce an infinite render." 1937 ) 1938 1939 self._sequencer.recording = True 1940 self._sequencer.record_filename = filename 1941 self._sequencer.render_mode = True 1942 self._sequencer.render_bars = bars if bars is not None else 0 1943 self._sequencer.render_max_seconds = max_minutes * 60.0 if max_minutes is not None else None 1944 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.
323def register_scale ( 324 name: str, 325 intervals: typing.List[int], 326 qualities: typing.Optional[typing.List[str]] = None 327) -> None: 328 329 """ 330 Register a custom scale for use with ``p.quantize()`` and 331 ``scale_pitch_classes()``. 332 333 Parameters: 334 name: Scale name (used in ``p.quantize(key, name)``). 335 intervals: Semitone offsets from the root (e.g. ``[0, 2, 3, 7, 8]`` 336 for Hirajōshi). Must start with 0 and contain values 0–11. 337 qualities: Optional chord quality per scale degree (e.g. 338 ``["minor", "major", "minor", "major", "diminished"]``). 339 Required only if you want to use the scale with 340 ``diatonic_chords()`` or ``diatonic_chord_sequence()``. 341 342 Example:: 343 344 import subsequence 345 346 subsequence.register_scale("raga_bhairav", [0, 1, 4, 5, 7, 8, 11]) 347 348 @comp.pattern(channel=0, length=4) 349 def melody (p): 350 p.note(60, beat=0) 351 p.quantize("C", "raga_bhairav") 352 """ 353 354 if not intervals or intervals[0] != 0: 355 raise ValueError("intervals must start with 0") 356 if any(i < 0 or i > 11 for i in intervals): 357 raise ValueError("intervals must contain values between 0 and 11") 358 if qualities is not None and len(qualities) != len(intervals): 359 raise ValueError( 360 f"qualities length ({len(qualities)}) must match " 361 f"intervals length ({len(intervals)})" 362 ) 363 364 INTERVAL_DEFINITIONS[name] = intervals 365 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: Root note name (``"C"``, ``"F#"``, ``"Bb"``, etc.). 222 mode: Scale mode name. Supports all keys of :data:`SCALE_MODE_MAP` 223 (e.g. ``"ionian"``, ``"dorian"``, ``"natural_minor"``, 224 ``"major_pentatonic"``). Use :func:`register_scale` for custom scales. 225 low: Lowest MIDI note (inclusive). When ``count`` is set, this is 226 the starting note from which the scale ascends. 227 high: Highest MIDI note (inclusive). Ignored when ``count`` is set. 228 count: Exact number of notes to return. Notes ascend from ``low`` 229 through successive scale degrees, cycling into higher octaves 230 as needed. When ``None`` (default), all scale tones between 231 ``low`` and ``high`` are returned. 232 233 Returns: 234 Sorted list of MIDI note numbers. 235 236 Examples: 237 ```python 238 import subsequence 239 import subsequence.constants.midi_notes as notes 240 241 # C major: all tones from middle C to C5 242 subsequence.scale_notes("C", "ionian", low=notes.C4, high=notes.C5) 243 # → [60, 62, 64, 65, 67, 69, 71, 72] 244 245 # E natural minor (aeolian) across one octave 246 subsequence.scale_notes("E", "aeolian", low=notes.E2, high=notes.E3) 247 # → [40, 42, 43, 45, 47, 48, 50, 52] 248 249 # 15 notes of A minor pentatonic ascending from A3 250 subsequence.scale_notes("A", "minor_pentatonic", low=notes.A3, count=15) 251 # → [57, 60, 62, 64, 67, 69, 72, 74, 76, 79, 81, 84, 86, 88, 91] 252 ``` 253 """ 254 255 key_pc = subsequence.chords.key_name_to_pc(key) 256 pcs = set(scale_pitch_classes(key_pc, mode)) 257 258 if count is not None: 259 if not pcs: 260 return [] 261 result: typing.List[int] = [] 262 pitch = low 263 while len(result) < count and pitch <= 127: 264 if pitch % 12 in pcs: 265 result.append(pitch) 266 pitch += 1 267 return result 268 269 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: Root note name (
"C","F#","Bb", etc.). - 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. - 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]
10def bank_select (bank: int) -> typing.Tuple[int, int]: 11 12 """ 13 Convert a 14-bit MIDI bank number to (MSB, LSB) for use with 14 ``p.program_change()``. 15 16 MIDI bank select uses two control-change messages: CC 0 (Bank MSB) and 17 CC 32 (Bank LSB). Together they encode a 14-bit bank number in the 18 range 0–16,383: 19 20 MSB = bank // 128 (upper 7 bits, sent on CC 0) 21 LSB = bank % 128 (lower 7 bits, sent on CC 32) 22 23 Args: 24 bank: Integer bank number, 0–16,383. Values outside this range are 25 clamped. 26 27 Returns: 28 ``(msb, lsb)`` tuple, each value in 0–127. 29 30 Example: 31 ```python 32 msb, lsb = subsequence.bank_select(128) # → (1, 0) 33 p.program_change(48, bank_msb=msb, bank_lsb=lsb) 34 ``` 35 """ 36 37 bank = max(0, min(16383, bank)) 38 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)