diff --git a/REVIVAL.md b/REVIVAL.md
index 4703658d..1740258f 100644
--- a/REVIVAL.md
+++ b/REVIVAL.md
@@ -1008,6 +1008,57 @@ GitHub Actions CI.
Phase 2 of `plans/tap.303.md` is **complete**; what remains for the 303 program is runtime
validation in Max and the deferred phase-3 sequencer.
+- ✅ **The sequencers — `tap.808.seq~` + `tap.303.seq~` (2026-07-18).** The coordinated
+ phase 3 both family plans deferred, designed and author-approved same day as
+ **`plans/tap.seq.md`** and shipped end to end: **one shared kernel engine**
+ (`step_seq.h`, `taptools::seq`) — phase-clocked from a `phasor~` ramp
+ (floor(phase × length), swing warping the odd steps, sample-accurate entries,
+ stateless against phase), 16 slots with cycle/step/now-quantized recall — with two
+ thin emitters speaking the shipped voice contracts verbatim. **`tap.808.seq~`**: one
+ drum row, impulses whose amplitude 0..1 is the accent (plain 0.01 = the 4 V base bus,
+ accented 0.5 = the accent knob at noon; provenance `tr808_kick.h`'s 4–14 V mapping),
+ `hits`/`accents`/`velocities` surfaces, `pulse` widening for envelope consumers.
+ **`tap.303.seq~`**: the acid line emitting the `tap.303~` pitch + gate pair (1.0/2.0
+ accent amplitudes; gate duty 0.5 per Open303's `AcidPattern`), **slide as gate-hold**
+ across the boundary — chained slides and the across-the-wrap slide pinned by test —
+ with per-step pitch/gate/accent/slide, live `transpose`, and the slide flag on the
+ *target* step per the package `note` convention (hardware stores it on the source;
+ divergence documented in the kernel header). Both rows: `length` 1..64 (polymeter off
+ one phasor — the triplet pre-scale generalized), swing (beyond-hardware, default 0),
+ `store`/`recall`, `todict`/`fromdict` dictionary patterns, a 1-based step-index UI
+ outlet (coalescing audio→scheduler handoff, the `tap.sift~` idiom). Kernel: 19 new
+ scenarios (suite 129/129), including the tb303-voice pairing test asserting slid
+ steps glide without retrigger. Wrappers: min-api unittests (compiled and run against
+ the mock kernel), maxrefs, help patchers, maxtest starters — and
+ **`help/tap.808.maxhelp` rebuilt on real rows** (one phasor, nine rows, the old
+ metro/counter/sel pattern retired), with `help/tap.303.seq~.maxhelp` superseding the
+ `tap.303~-pattern.maxpat` dry run (which stays as the event-domain alternative).
+ **Runtime validation in Max still pending**, as everywhere.
+
+- ✅ **The sequencer verification notebook (2026-07-18).** **`notebooks/step_seq.ipynb`**
+ (kernel repo), house pattern: the shipping `step_seq.h` driven through the C ABI
+ (extended with `taptools_seqtrig`/`taptools_seqnote` + a minimal `taptools_kick`).
+ Six executed, asserted sections: the analytic grid (≤ 1 sample; polymeter), the swing
+ warp (odd-step delay = swing/2), the trigger-bus levels (0.01/0.5/1.0) + `pulse_ms`,
+ the 303 line signals (duty 0.5, accent 2.0, gate-hold slide — including the
+ full-step hold on a slide *source* step, a case the Catch2 suite's isolated-step
+ scenario didn't exhibit), **four bars of acid + kick rendered from the real
+ `tb303_voice.h`/`tr808_kick.h` off one phasor ramp** (embedded audio), and
+ cycle-quantized recall swapping exactly on the wrap sample.
+
+- ✅ **"Tools on Tap" Part V — The rhythm section (2026-07-18).** Two field-guide
+ chapters in the kernel repo's book: *The acid machine* (`tap.diode~`/`tap.303~`/
+ `tap.303.seq~`) and *The drum machine* (the eight `tap.808.*` channels +
+ `tap.808.seq~`), every number sourced from the executed notebooks or the kernel
+ suite per the book's own rules; the machine deep-dives renumber to Part VI —
+ which same-day gained its own four rhythm-section chapters: *Seventeen, not four*
+ (`diode_ladder.h` — the k = 17 threshold derived, the closed-form coupled ZDF solve,
+ the WDF no-go), *The couplings are the instrument* (`tb303_voice.h` — the C13 wow as
+ three lines, the measured envmod law, the documented Open303 divergence), *One
+ network, eight voices* (the `tr808_*` headers — the bridged-T solved on its states,
+ the calibration lesson), and *Time as a function of phase* (`step_seq.h` — the O(1)
+ derivation, gate-hold look-ahead, armed-recall re-derivation).
+
---
## 8. The `taptools-min` reconciliation (2026-06-17)
@@ -1149,11 +1200,12 @@ Roland TR-808's analog drum voices: the eight voice channels
shared kernel blocks: bridged-T resonator, metal bank, swing-VCA), the family accent-bus
contract (signal-edge amplitude = accent on the 4–14 V trigger bus), the family overview
patcher, and the kernel's `tr808_render` tool. Provenance: the Werner–Abel–Smith
-DAFx-14/ICMC papers + the TR-808 Service Notes schematics, read directly. Flagged WDF
-circuit-simulation upgrade path (`@circuit`, the `svf.h` two-circuit pattern) and a
-potential later sequencer phase (`tap.808.seq~`) remain. See the §7 progress-log
-entries (slices 1–5). Next: runtime validation in Max, §7.2 sample-pack calibration,
-then the Phase 2 WDF go/no-go on the kick.
+DAFx-14/ICMC papers + the TR-808 Service Notes schematics, read directly. **The phase-3
+sequencer shipped 2026-07-18** as `tap.808.seq~` (see `plans/tap.seq.md` and the §7
+entry — one row per voice, phase-clocked, sharing its engine with `tap.303.seq~`).
+Flagged WDF circuit-simulation upgrade path (`@circuit`, the `svf.h` two-circuit
+pattern) remains. See the §7 progress-log entries (slices 1–5, §7.2 calibration).
+Next: runtime validation in Max, then the Phase 2 WDF go/no-go on the kick.
**11. Net-new object family — `tap.303.*` (2026-07-17).** 📋 **Plan approved; slices 1–2
shipped** — see **`plans/tap.303.md`**. The TB-303 recreation, companion to the `tap.808.*`
@@ -1166,9 +1218,10 @@ circuit documentation, Open303, x0xb0x schematics, Service Notes. **Phases 1 and
seed/tolerance + factory presets; phase 2: the `vca clean|warm` transistor stage, the
square shaper resolved via Open303's measured constants, and the author-approved WDF no-go
— grounded in the kernel repo's **`notebooks/tb303.ipynb`** verification notebook; see the
-§7 entries). Remaining: the deferred sequencer phase (`tap.303.seq~`, coordinated with
-`tap.808.seq~`) and **runtime validation in Max** for both objects (help patchers,
-maxtests, the render WAVs, and the notebook as the evaluation material).
+§7 entries). **The phase-3 sequencer shipped 2026-07-18** as `tap.303.seq~` (see
+`plans/tap.seq.md` and the §7 entry — the acid line on the shared step engine, slide as
+gate-hold). Remaining: **runtime validation in Max** for all three objects (help
+patchers, maxtests, the render WAVs, and the notebook as the evaluation material).
Remaining (ongoing, now cross-repo — DSP lands in `tap/taptools`, then bump the submodule pin
here): lift the remaining simple inline-DSP objects' math into kernel headers opportunistically as
diff --git a/docs/tap.303.seq~.maxref.xml b/docs/tap.303.seq~.maxref.xml
new file mode 100644
index 00000000..feebc165
--- /dev/null
+++ b/docs/tap.303.seq~.maxref.xml
@@ -0,0 +1,212 @@
+
+
+
+
+
+
+ Phase-clocked acid-line sequencer for tap.303~
+
+
+
+ The tap.303.seq~ object is a step-sequenced bass line speaking exactly the
+ tap.303~ note contract: feed it a phase ramp (0..1 per pattern cycle — a
+ phasor~) and it emits the voice's inlet pair, a pitch signal (MIDI note number,
+ held between notes) and a gate signal whose edge amplitude encodes accent (1.0 plain,
+ 2.0 accented; the accent amount stays on the voice's knob, like the hardware). The gate
+ opens at the step start and closes halfway through the step (Open303's measured duty).
+ A step with the slide flag is approached legato: the gate holds through the boundary
+ while the pitch steps, and the voice's ~60 ms glide does the rest — no retrigger,
+ chained slides chain, and a slide across the pattern wrap works. Per-step data mirrors
+ the hardware pattern memory (pitch, gate/rest, accent, slide; pitch as a MIDI float
+ subsumes the octave up/down flags), without recreating the 1981 entry workflow:
+ patterns are lists, step messages, sixteen slots with boundary-quantized recall, and
+ dictionaries. Rows share the phase-clock idiom with tap.808.seq~ — one phasor
+ runs a whole rhythm section in sample lock, and this row's gate edge also triggers any
+ tap.808.* voice directly. The design of record is plans/tap.seq.md.
+
+
+
+
+
+ 74 Objects
+ TapTools
+ Audio
+ Sequencing
+ Synthesis
+
+
+
+
+
+
+ Phase ramp: 0..1 per pattern cycle (connect phasor~)
+
+
+
+
+ Pitch as MIDI note number — to tap.303~'s left inlet
+
+
+ Gate; edge amplitude 1..2 encodes accent — to tap.303~'s right inlet
+
+
+ Current step number (1-based), sent at each step change
+
+
+
+
+
+
+
+
+
+ The phase ramp. The row derives the current step from the phase (swing warps the
+ odd-step starts) and plays the pattern sample-accurately. Stateless against
+ phase: jump, scrub, or reverse the ramp and the line follows.
+
+
+
+
+
+
+
+
+
+
+ Set one step and gate it on: step number (1..64), pitch as a MIDI note number
+ (fractional = cents), then optional accent and slide flags (0/1). A slide flag
+ means the step is approached legato from the note before it.
+
+
+
+
+
+
+
+ Silence one step (its pitch and flags are kept).
+
+
+
+
+
+
+
+ Set per-step pitches as MIDI note numbers (fractional = cents).
+
+
+
+
+
+
+
+ Set which steps sound: one 0/1 per step (0 = rest).
+
+
+
+
+
+
+
+ Set per-step accent flags: accented steps gate at 2.0 (the voice reads accent
+ depth = amplitude - 1).
+
+
+
+
+
+
+
+ Set per-step slide flags. A flagged step is approached legato: the gate holds
+ through the boundary and the voice glides to it without retriggering.
+
+
+
+
+
+
+
+ Store the pattern in a slot (1..16).
+
+
+
+
+
+
+
+ Recall a slot (1..16); it takes over at the quantize boundary (default: the next
+ pattern top).
+
+
+
+
+
+
+
+ Write the pattern (length, pitches, gates, accents, slides) into a named
+ dictionary, for saving with the patcher or passing around as data.
+
+
+
+
+
+
+
+ Restore a pattern previously written with todict.
+
+
+
+
+
+ Empty the pattern (all rests) and reset the clock state.
+
+
+
+
+
+
+
+
+ Steps per pattern cycle (1..64, default 16)
+
+ Steps per pattern cycle. A different length than the other rows on the same
+ phasor is polymeter.
+
+
+
+ Swing 0..1 (0 = straight; 0.67 = triplet shuffle)
+
+ Odd-numbered steps play late by up to half a step. The hardware had no swing —
+ 0 is stock.
+
+
+
+ Live transposition in semitones (-48..48)
+
+ Applied as notes fire — what the hardware's transpose mode did, without the mode.
+
+
+
+ When a recalled pattern takes over: cycle (default), step, or now
+
+ cycle swaps at the next pattern top, step at the next step boundary, now
+ immediately.
+
+
+
+ Force the gate low (the clock keeps running; pitch holds)
+
+ Mutes the gate without stopping the row, so unmuting re-enters in place.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/tap.808.seq~.maxref.xml b/docs/tap.808.seq~.maxref.xml
new file mode 100644
index 00000000..e48625e7
--- /dev/null
+++ b/docs/tap.808.seq~.maxref.xml
@@ -0,0 +1,208 @@
+
+
+
+
+
+
+ Phase-clocked step-sequencer drum row for the tap.808 family
+
+
+
+ The tap.808.seq~ object is one drum row of a step sequencer: feed it a phase
+ ramp (0..1 per pattern cycle — a phasor~, transport-locked if you like) and it
+ emits a trigger impulse at each sounding step whose amplitude 0..1 is the accent.
+ That is exactly the tap.808.* family trigger contract (amplitude maps onto the
+ hardware's common 4-14 V bus), so a row wires straight into any of the eight voices —
+ or into tap.adsr~ and anything else edge-triggered (the pulse attribute widens
+ the impulse into a held gate for envelope consumers). Because the row derives its step
+ from the phase, every row fed by one phasor stays sample-locked forever; a row with a
+ different length runs in polymeter, which subsumes the hardware's triplet pre-scale.
+ Steps are velocities 0..1 (0 = rest), or the hardware's on/accent view via the hits
+ and accents lists mapped onto the plain and accented trigger levels. Sixteen pattern
+ slots with store/recall, quantized to the next cycle boundary by default, are the
+ hardware's A/B-half and basic/fill switching as one message. The rightmost outlet
+ reports the current step number (1-based) for displays. The design of record is
+ plans/tap.seq.md; the engine is shared with tap.303.seq~.
+
+
+
+
+
+ 74 Objects
+ TapTools
+ Audio
+ Sequencing
+ Drums
+
+
+
+
+
+
+ Phase ramp: 0..1 per pattern cycle (connect phasor~)
+
+
+
+
+ Trigger impulses; amplitude 0..1 is the accent (the tap.808.* contract)
+
+
+ Current step number (1-based), sent at each step change
+
+
+
+
+
+
+
+
+
+ The phase ramp. The row derives the current step as floor(phase x length), with
+ swing warping the odd-step starts, and fires each sounding step's trigger on the
+ sample its boundary crosses. The row is stateless against phase: jump, scrub, or
+ reverse the ramp and the steps follow.
+
+
+
+
+
+
+
+ Set per-step trigger levels directly: one float 0..1 per step, 0 for a rest.
+
+
+
+
+
+
+
+ The hardware view: one 0/1 per step — which steps sound. Sounding steps take the
+ plain trigger level (or the accented level where the accent flag is set).
+ Overwrites the row's velocities.
+
+
+
+
+
+
+
+ The hardware view: one 0/1 per step — which sounding steps are accented (the
+ accented trigger level). Overwrites the row's velocities.
+
+
+
+
+
+
+
+
+ Set one step's velocity: step number (1..64) then level (0..1, 0 = rest).
+
+
+
+
+
+
+
+ Store the pattern in a slot (1..16).
+
+
+
+
+
+
+
+ Recall a slot (1..16). The new pattern takes over at the quantize boundary —
+ by default the next pattern top, which is the hardware's A/B-half and fill move.
+
+
+
+
+
+
+
+ Write the pattern (length and velocities) into a named dictionary, for saving
+ with the patcher or passing around as data.
+
+
+
+
+
+
+
+ Restore a pattern previously written with todict.
+
+
+
+
+
+ Empty the pattern (all rests) and reset the clock state.
+
+
+
+
+
+
+
+
+ Steps per pattern cycle (1..64, default 16)
+
+ Steps per pattern cycle. Rows sharing one phasor with different lengths run in
+ polymeter — 12 against 16 is the hardware's triplet pre-scale and then some.
+
+
+
+ Swing 0..1 (0 = straight; 0.67 = triplet shuffle)
+
+ Odd-numbered steps play late by up to half a step. The hardware had no swing —
+ 0 is stock.
+
+
+
+ Trigger level for un-accented hits (default 0.01 = the 4 V base)
+
+ The amplitude emitted for un-accented hits. The default is the hardware's 4 V
+ base trigger — accent ~0, kept just above the voices' edge threshold.
+
+
+
+ Trigger level for accented hits (default 0.5 = accent knob at noon)
+
+ The amplitude emitted for accented hits: 0.5 is the hardware's accent level knob
+ at noon, 1.0 the full 14 V bus.
+
+
+
+ Impulse width in ms (0 = single-sample edge, the default)
+
+ Widths above 0 hold the trigger as a gate — useful for driving envelopes. Keep it
+ shorter than a step at your clock rate, or back-to-back triggers merge and
+ downstream edge detectors miss the second one.
+
+
+
+ When a recalled pattern takes over: cycle (default), step, or now
+
+ cycle swaps at the next pattern top (the hardware feel), step at the next step
+ boundary, now immediately.
+
+
+
+ Silence the trigger output (the clock keeps running)
+
+ Mutes the output without stopping the row, so unmuting re-enters in place.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/help/tap.303.seq~.maxhelp b/help/tap.303.seq~.maxhelp
new file mode 100644
index 00000000..61dc3df2
--- /dev/null
+++ b/help/tap.303.seq~.maxhelp
@@ -0,0 +1,716 @@
+{
+ "patcher": {
+ "fileversion": 1,
+ "appversion": {
+ "major": 8,
+ "minor": 6,
+ "revision": 2,
+ "architecture": "x64",
+ "modernui": 1
+ },
+ "classnamespace": "box",
+ "rect": [
+ 120.0,
+ 80.0,
+ 900.0,
+ 720.0
+ ],
+ "bglocked": 0,
+ "openinpresentation": 0,
+ "default_fontsize": 11.0,
+ "default_fontface": 0,
+ "default_fontname": "Arial",
+ "gridonopen": 1,
+ "gridsize": [
+ 15.0,
+ 15.0
+ ],
+ "gridsnaponopen": 1,
+ "statusbarvisible": 2,
+ "toolbarvisible": 1,
+ "description": "tap.303.seq~: the acid line driving tap.303~ sample-accurately \u2014 accent, slide (gate-hold legato), transpose, swing, slots.",
+ "boxes": [
+ {
+ "box": {
+ "id": "obj-1",
+ "maxclass": "comment",
+ "numinlets": 1,
+ "numoutlets": 1,
+ "outlettype": [
+ ""
+ ],
+ "patching_rect": [
+ 10.0,
+ 8.0,
+ 300.0,
+ 20.0
+ ],
+ "text": "tap.303.seq~ \u2014 a step-sequenced acid line"
+ }
+ },
+ {
+ "box": {
+ "id": "obj-2",
+ "maxclass": "comment",
+ "numinlets": 1,
+ "numoutlets": 1,
+ "outlettype": [
+ ""
+ ],
+ "patching_rect": [
+ 10.0,
+ 32.0,
+ 860.0,
+ 60.0
+ ],
+ "text": "The row emits tap.303~'s inlet pair: pitch (MIDI note) and a gate whose edge amplitude encodes accent (1 plain, 2 accented). A step with the slide flag is approached legato \u2014 the gate holds through the boundary and the voice glides (~60 ms) without retriggering, exactly the hardware. This pattern is the tap.303~-pattern.maxpat demo line, now sample-accurate."
+ }
+ },
+ {
+ "box": {
+ "id": "obj-3",
+ "maxclass": "newobj",
+ "numinlets": 2,
+ "numoutlets": 1,
+ "outlettype": [
+ "signal"
+ ],
+ "patching_rect": [
+ 10.0,
+ 108.0,
+ 110.0,
+ 22.0
+ ],
+ "text": "phasor~ 0.4845"
+ }
+ },
+ {
+ "box": {
+ "id": "obj-4",
+ "maxclass": "comment",
+ "numinlets": 1,
+ "numoutlets": 1,
+ "outlettype": [
+ ""
+ ],
+ "patching_rect": [
+ 130.0,
+ 108.0,
+ 380.0,
+ 20.0
+ ],
+ "text": "<- one ramp per pattern; the same phasor can drive tap.808.seq~ rows in lock"
+ }
+ },
+ {
+ "box": {
+ "id": "obj-5",
+ "maxclass": "message",
+ "numinlets": 2,
+ "numoutlets": 1,
+ "outlettype": [
+ ""
+ ],
+ "patching_rect": [
+ 10.0,
+ 150.0,
+ 850.0,
+ 20.0
+ ],
+ "text": "pitches 33 33 45 33 33 36 33 40 38 33 33 45 33 36 35 31 \\, gates 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 \\, accents 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 \\, slides 0 0 1 0 0 0 0 1 0 0 0 0 0 1 0 0"
+ }
+ },
+ {
+ "box": {
+ "id": "obj-6",
+ "maxclass": "newobj",
+ "numinlets": 1,
+ "numoutlets": 1,
+ "outlettype": [
+ "bang"
+ ],
+ "patching_rect": [
+ 870.0,
+ 108.0,
+ 70.0,
+ 22.0
+ ],
+ "text": "loadbang"
+ }
+ },
+ {
+ "box": {
+ "id": "obj-7",
+ "maxclass": "comment",
+ "numinlets": 1,
+ "numoutlets": 1,
+ "outlettype": [
+ ""
+ ],
+ "patching_rect": [
+ 10.0,
+ 178.0,
+ 640.0,
+ 20.0
+ ],
+ "text": "^ the whole line as lists \u2014 or per step: step [accent] [slide], rest "
+ }
+ },
+ {
+ "box": {
+ "id": "obj-8",
+ "maxclass": "message",
+ "numinlets": 2,
+ "numoutlets": 1,
+ "outlettype": [
+ ""
+ ],
+ "patching_rect": [
+ 10.0,
+ 206.0,
+ 130.0,
+ 20.0
+ ],
+ "text": "step 3 45 0 1"
+ }
+ },
+ {
+ "box": {
+ "id": "obj-9",
+ "maxclass": "message",
+ "numinlets": 2,
+ "numoutlets": 1,
+ "outlettype": [
+ ""
+ ],
+ "patching_rect": [
+ 150.0,
+ 206.0,
+ 50.0,
+ 20.0
+ ],
+ "text": "rest 3"
+ }
+ },
+ {
+ "box": {
+ "id": "obj-10",
+ "maxclass": "message",
+ "numinlets": 2,
+ "numoutlets": 1,
+ "outlettype": [
+ ""
+ ],
+ "patching_rect": [
+ 220.0,
+ 206.0,
+ 90.0,
+ 20.0
+ ],
+ "text": "transpose 12"
+ }
+ },
+ {
+ "box": {
+ "id": "obj-11",
+ "maxclass": "message",
+ "numinlets": 2,
+ "numoutlets": 1,
+ "outlettype": [
+ ""
+ ],
+ "patching_rect": [
+ 320.0,
+ 206.0,
+ 90.0,
+ 20.0
+ ],
+ "text": "transpose 0"
+ }
+ },
+ {
+ "box": {
+ "id": "obj-12",
+ "maxclass": "message",
+ "numinlets": 2,
+ "numoutlets": 1,
+ "outlettype": [
+ ""
+ ],
+ "patching_rect": [
+ 420.0,
+ 206.0,
+ 80.0,
+ 20.0
+ ],
+ "text": "swing 0.61"
+ }
+ },
+ {
+ "box": {
+ "id": "obj-13",
+ "maxclass": "message",
+ "numinlets": 2,
+ "numoutlets": 1,
+ "outlettype": [
+ ""
+ ],
+ "patching_rect": [
+ 510.0,
+ 206.0,
+ 60.0,
+ 20.0
+ ],
+ "text": "store 2"
+ }
+ },
+ {
+ "box": {
+ "id": "obj-14",
+ "maxclass": "message",
+ "numinlets": 2,
+ "numoutlets": 1,
+ "outlettype": [
+ ""
+ ],
+ "patching_rect": [
+ 580.0,
+ 206.0,
+ 60.0,
+ 20.0
+ ],
+ "text": "recall 2"
+ }
+ },
+ {
+ "box": {
+ "id": "obj-15",
+ "maxclass": "newobj",
+ "numinlets": 1,
+ "numoutlets": 3,
+ "outlettype": [
+ "signal",
+ "signal",
+ ""
+ ],
+ "patching_rect": [
+ 10.0,
+ 250.0,
+ 200.0,
+ 22.0
+ ],
+ "text": "tap.303.seq~"
+ }
+ },
+ {
+ "box": {
+ "id": "obj-16",
+ "maxclass": "number",
+ "numinlets": 1,
+ "numoutlets": 2,
+ "outlettype": [
+ "",
+ "bang"
+ ],
+ "patching_rect": [
+ 230.0,
+ 250.0,
+ 50.0,
+ 22.0
+ ]
+ }
+ },
+ {
+ "box": {
+ "id": "obj-17",
+ "maxclass": "comment",
+ "numinlets": 1,
+ "numoutlets": 1,
+ "outlettype": [
+ ""
+ ],
+ "patching_rect": [
+ 287.0,
+ 250.0,
+ 200.0,
+ 20.0
+ ],
+ "text": "<- current step, for displays"
+ }
+ },
+ {
+ "box": {
+ "id": "obj-18",
+ "maxclass": "newobj",
+ "numinlets": 2,
+ "numoutlets": 1,
+ "outlettype": [
+ "signal"
+ ],
+ "patching_rect": [
+ 10.0,
+ 300.0,
+ 430.0,
+ 22.0
+ ],
+ "text": "tap.303~ @cutoff 500 @resonance 0.95 @envmod 0.65 @decay 300 @accent 0.9"
+ }
+ },
+ {
+ "box": {
+ "id": "obj-19",
+ "maxclass": "comment",
+ "numinlets": 1,
+ "numoutlets": 1,
+ "outlettype": [
+ ""
+ ],
+ "patching_rect": [
+ 10.0,
+ 330.0,
+ 560.0,
+ 20.0
+ ],
+ "text": "^ pitch -> left inlet, gate -> right inlet; the voice's accent knob decides how hard accents hit; its gate edge can also trigger any tap.808.* voice directly"
+ }
+ },
+ {
+ "box": {
+ "id": "obj-20",
+ "maxclass": "message",
+ "numinlets": 2,
+ "numoutlets": 1,
+ "outlettype": [
+ ""
+ ],
+ "patching_rect": [
+ 450.0,
+ 268.0,
+ 56.0,
+ 20.0
+ ],
+ "text": "recall 1"
+ }
+ },
+ {
+ "box": {
+ "id": "obj-21",
+ "maxclass": "message",
+ "numinlets": 2,
+ "numoutlets": 1,
+ "outlettype": [
+ ""
+ ],
+ "patching_rect": [
+ 514.0,
+ 268.0,
+ 56.0,
+ 20.0
+ ],
+ "text": "recall 3"
+ }
+ },
+ {
+ "box": {
+ "id": "obj-22",
+ "maxclass": "message",
+ "numinlets": 2,
+ "numoutlets": 1,
+ "outlettype": [
+ ""
+ ],
+ "patching_rect": [
+ 578.0,
+ 268.0,
+ 56.0,
+ 20.0
+ ],
+ "text": "recall 6"
+ }
+ },
+ {
+ "box": {
+ "id": "obj-23",
+ "maxclass": "comment",
+ "numinlets": 1,
+ "numoutlets": 1,
+ "outlettype": [
+ ""
+ ],
+ "patching_rect": [
+ 640.0,
+ 268.0,
+ 260.0,
+ 20.0
+ ],
+ "text": "<- voice factory presets morph under the line"
+ }
+ },
+ {
+ "box": {
+ "id": "obj-24",
+ "maxclass": "newobj",
+ "numinlets": 2,
+ "numoutlets": 1,
+ "outlettype": [
+ "signal"
+ ],
+ "patching_rect": [
+ 10.0,
+ 366.0,
+ 40.0,
+ 22.0
+ ],
+ "text": "*~ 0.8"
+ }
+ },
+ {
+ "box": {
+ "id": "obj-25",
+ "maxclass": "ezdac~",
+ "numinlets": 2,
+ "numoutlets": 0,
+ "outlettype": [],
+ "patching_rect": [
+ 10.0,
+ 408.0,
+ 45.0,
+ 45.0
+ ]
+ }
+ }
+ ],
+ "lines": [
+ {
+ "patchline": {
+ "source": [
+ "obj-3",
+ 0
+ ],
+ "destination": [
+ "obj-15",
+ 0
+ ]
+ }
+ },
+ {
+ "patchline": {
+ "source": [
+ "obj-6",
+ 0
+ ],
+ "destination": [
+ "obj-5",
+ 0
+ ]
+ }
+ },
+ {
+ "patchline": {
+ "source": [
+ "obj-5",
+ 0
+ ],
+ "destination": [
+ "obj-15",
+ 0
+ ]
+ }
+ },
+ {
+ "patchline": {
+ "source": [
+ "obj-8",
+ 0
+ ],
+ "destination": [
+ "obj-15",
+ 0
+ ]
+ }
+ },
+ {
+ "patchline": {
+ "source": [
+ "obj-9",
+ 0
+ ],
+ "destination": [
+ "obj-15",
+ 0
+ ]
+ }
+ },
+ {
+ "patchline": {
+ "source": [
+ "obj-10",
+ 0
+ ],
+ "destination": [
+ "obj-15",
+ 0
+ ]
+ }
+ },
+ {
+ "patchline": {
+ "source": [
+ "obj-11",
+ 0
+ ],
+ "destination": [
+ "obj-15",
+ 0
+ ]
+ }
+ },
+ {
+ "patchline": {
+ "source": [
+ "obj-12",
+ 0
+ ],
+ "destination": [
+ "obj-15",
+ 0
+ ]
+ }
+ },
+ {
+ "patchline": {
+ "source": [
+ "obj-13",
+ 0
+ ],
+ "destination": [
+ "obj-15",
+ 0
+ ]
+ }
+ },
+ {
+ "patchline": {
+ "source": [
+ "obj-14",
+ 0
+ ],
+ "destination": [
+ "obj-15",
+ 0
+ ]
+ }
+ },
+ {
+ "patchline": {
+ "source": [
+ "obj-15",
+ 0
+ ],
+ "destination": [
+ "obj-18",
+ 0
+ ]
+ }
+ },
+ {
+ "patchline": {
+ "source": [
+ "obj-15",
+ 1
+ ],
+ "destination": [
+ "obj-18",
+ 1
+ ]
+ }
+ },
+ {
+ "patchline": {
+ "source": [
+ "obj-15",
+ 2
+ ],
+ "destination": [
+ "obj-16",
+ 0
+ ]
+ }
+ },
+ {
+ "patchline": {
+ "source": [
+ "obj-20",
+ 0
+ ],
+ "destination": [
+ "obj-18",
+ 0
+ ]
+ }
+ },
+ {
+ "patchline": {
+ "source": [
+ "obj-21",
+ 0
+ ],
+ "destination": [
+ "obj-18",
+ 0
+ ]
+ }
+ },
+ {
+ "patchline": {
+ "source": [
+ "obj-22",
+ 0
+ ],
+ "destination": [
+ "obj-18",
+ 0
+ ]
+ }
+ },
+ {
+ "patchline": {
+ "source": [
+ "obj-18",
+ 0
+ ],
+ "destination": [
+ "obj-24",
+ 0
+ ]
+ }
+ },
+ {
+ "patchline": {
+ "source": [
+ "obj-24",
+ 0
+ ],
+ "destination": [
+ "obj-25",
+ 0
+ ]
+ }
+ },
+ {
+ "patchline": {
+ "source": [
+ "obj-24",
+ 0
+ ],
+ "destination": [
+ "obj-25",
+ 1
+ ]
+ }
+ }
+ ],
+ "dependency_cache": [],
+ "autosave": 0
+ }
+}
diff --git a/help/tap.808.maxhelp b/help/tap.808.maxhelp
index c00eb080..6544c565 100644
--- a/help/tap.808.maxhelp
+++ b/help/tap.808.maxhelp
@@ -3,48 +3,49 @@
"fileversion": 1,
"appversion": {
"major": 8,
- "minor": 5,
- "revision": 0,
+ "minor": 6,
+ "revision": 2,
"architecture": "x64",
"modernui": 1
},
+ "classnamespace": "box",
"rect": [
- 50.0,
- 50.0,
- 1120.0,
- 590.0
+ 100.0,
+ 60.0,
+ 980.0,
+ 780.0
],
"bglocked": 0,
"openinpresentation": 0,
"default_fontsize": 11.0,
"default_fontface": 0,
- "default_fontname": "Verdana",
+ "default_fontname": "Arial",
"gridonopen": 1,
"gridsize": [
- 5.0,
- 5.0
+ 15.0,
+ 15.0
],
"gridsnaponopen": 1,
"statusbarvisible": 2,
"toolbarvisible": 1,
- "description": "the tap.808 family: eight TR-808 voice channels on one accent bus.",
+ "description": "The tap.808 family overview: eight voices on a two-bar pattern, sequenced sample-accurately by tap.808.seq~ rows off one phasor~.",
"boxes": [
{
"box": {
"id": "obj-1",
"maxclass": "comment",
"numinlets": 1,
- "numoutlets": 0,
+ "numoutlets": 1,
+ "outlettype": [
+ ""
+ ],
"patching_rect": [
- 20.0,
- 14.0,
+ 10.0,
+ 8.0,
300.0,
- 27.0
+ 20.0
],
- "text": "the tap.808 family",
- "fontname": "Verdana",
- "fontsize": 18.0,
- "fontface": 1
+ "text": "the tap.808 family"
}
},
{
@@ -52,16 +53,17 @@
"id": "obj-2",
"maxclass": "comment",
"numinlets": 1,
- "numoutlets": 0,
+ "numoutlets": 1,
+ "outlettype": [
+ ""
+ ],
"patching_rect": [
- 20.0,
- 42.0,
- 560.0,
- 18.0
+ 10.0,
+ 32.0,
+ 860.0,
+ 20.0
],
- "text": "Eight circuit-informed TR-808 voice channels sharing one trigger contract.",
- "fontname": "Verdana",
- "fontsize": 11.0
+ "text": "Eight circuit-informed TR-808 voice channels sharing one trigger contract, sequenced by tap.808.seq~ rows."
}
},
{
@@ -69,161 +71,159 @@
"id": "obj-3",
"maxclass": "comment",
"numinlets": 1,
- "numoutlets": 0,
+ "numoutlets": 1,
+ "outlettype": [
+ ""
+ ],
"patching_rect": [
- 20.0,
- 64.0,
- 1060.0,
- 72.0
+ 10.0,
+ 56.0,
+ 860.0,
+ 120.0
],
- "text": "Every voice speaks the same accent bus: a signal rising edge fires it sample-accurately, and the edge's amplitude (0..1) is the accent \u2014 modeled on the hardware's common 4-14 V trigger bus, so an accent is a harder hit into the circuit, not a volume knob. From the scheduler: bang = full accent, trigger 0.6 = accented hit, clear = reset. Voice state persists across triggers (no machine-gunning), and every noise source is seeded and deterministic \u2014 give mc. instances different @seed values. Each voice's own help file covers its panel controls.",
- "fontname": "Verdana",
- "fontsize": 11.0
+ "text": "Every voice speaks the same accent bus: a signal rising edge fires it sample-accurately, and the edge's amplitude (0..1) is the accent \u2014 modeled on the hardware's common 4-14 V trigger bus, so an accent is a harder hit into the circuit, not a volume knob. Each tap.808.seq~ row emits exactly that: per-step velocities on one phase ramp, so the whole kit runs sample-locked off the single phasor~ below (the hardware's sequencer as patch cords \u2014 swing it, run rows at different lengths for polymeter, or recall pattern slots at the cycle boundary for fills). Voice state persists across triggers (no machine-gunning), and every noise source is seeded and deterministic \u2014 give mc. instances different @seed values. Each voice's own help file covers its panel controls; see tap.808.seq~.maxhelp for the row's full surface."
}
},
{
"box": {
"id": "obj-4",
- "maxclass": "toggle",
- "numinlets": 1,
+ "maxclass": "newobj",
+ "numinlets": 2,
"numoutlets": 1,
+ "outlettype": [
+ "signal"
+ ],
"patching_rect": [
- 20.0,
- 152.0,
- 20.0,
- 20.0
+ 10.0,
+ 190.0,
+ 110.0,
+ 22.0
],
- "outlettype": [
- "int"
- ]
+ "text": "phasor~ 0.4845"
}
},
{
"box": {
"id": "obj-5",
- "maxclass": "newobj",
+ "maxclass": "comment",
"numinlets": 1,
"numoutlets": 1,
+ "outlettype": [
+ ""
+ ],
"patching_rect": [
- 48.0,
- 152.0,
- 70.0,
+ 130.0,
+ 190.0,
+ 480.0,
20.0
],
- "outlettype": [
- "bang"
- ],
- "text": "metro 129",
- "fontname": "Verdana",
- "fontsize": 11.0
+ "text": "<- one ramp per bar: sixteenths at ~116 BPM, all rows in sample lock"
}
},
{
"box": {
"id": "obj-6",
- "maxclass": "newobj",
- "numinlets": 1,
- "numoutlets": 4,
- "patching_rect": [
- 126.0,
- 152.0,
- 84.0,
- 20.0
- ],
+ "maxclass": "gain~",
+ "numinlets": 2,
+ "numoutlets": 2,
"outlettype": [
- "int",
- "",
- "",
- "int"
+ "signal",
+ ""
],
- "text": "counter 1 16",
- "fontname": "Verdana",
- "fontsize": 11.0
+ "patching_rect": [
+ 700.0,
+ 560.0,
+ 30.0,
+ 80.0
+ ]
}
},
{
"box": {
"id": "obj-7",
- "maxclass": "comment",
- "numinlets": 1,
+ "maxclass": "ezdac~",
+ "numinlets": 2,
"numoutlets": 0,
+ "outlettype": [],
"patching_rect": [
- 220.0,
- 154.0,
- 280.0,
- 18.0
- ],
- "text": "<- a two-bar pattern: sixteenths at 116 BPM",
- "fontname": "Verdana",
- "fontsize": 11.0
+ 700.0,
+ 660.0,
+ 45.0,
+ 45.0
+ ]
}
},
{
"box": {
"id": "obj-8",
- "maxclass": "gain~",
- "numinlets": 2,
- "numoutlets": 2,
+ "maxclass": "comment",
+ "numinlets": 1,
+ "numoutlets": 1,
+ "outlettype": [
+ ""
+ ],
"patching_rect": [
- 20.0,
- 428.0,
- 30.0,
- 60.0
+ 750.0,
+ 580.0,
+ 180.0,
+ 20.0
],
- "outlettype": [
- "signal",
- "int"
- ]
+ "text": "<- all eight voices sum here"
}
},
{
"box": {
"id": "obj-9",
- "maxclass": "ezdac~",
- "numinlets": 2,
- "numoutlets": 0,
+ "maxclass": "comment",
+ "numinlets": 1,
+ "numoutlets": 1,
+ "outlettype": [
+ ""
+ ],
"patching_rect": [
- 20.0,
- 502.0,
- 40.0,
- 40.0
+ 10.0,
+ 230.0,
+ 150.0,
+ 20.0
],
- "local": 1
+ "text": "kick"
}
},
{
"box": {
"id": "obj-10",
- "maxclass": "comment",
+ "maxclass": "newobj",
"numinlets": 1,
- "numoutlets": 0,
+ "numoutlets": 1,
+ "outlettype": [
+ ""
+ ],
"patching_rect": [
- 70.0,
- 512.0,
- 520.0,
- 18.0
+ 10.0,
+ 254.0,
+ 160.0,
+ 22.0
],
- "text": "<- all eight voices sum here; the messages are also clickable to audition a voice",
- "fontname": "Verdana",
- "fontsize": 11.0
+ "text": "loadmess velocities 1. 0 0 0 0.75 0 0 0 0.75 0 0 0 0.75 0 0 0"
}
},
{
"box": {
"id": "obj-11",
- "maxclass": "comment",
+ "maxclass": "newobj",
"numinlets": 1,
- "numoutlets": 0,
+ "numoutlets": 2,
+ "outlettype": [
+ "signal",
+ ""
+ ],
"patching_rect": [
- 20.0,
- 196.0,
- 120.0,
- 18.0
+ 10.0,
+ 278.0,
+ 150.0,
+ 22.0
],
- "text": "kick",
- "fontname": "Verdana",
- "fontsize": 11.0,
- "fontface": 1
+ "text": "tap.808.seq~"
}
},
{
@@ -232,120 +232,107 @@
"maxclass": "newobj",
"numinlets": 1,
"numoutlets": 1,
- "patching_rect": [
- 20.0,
- 372.0,
- 101.0,
- 21.0
- ],
"outlettype": [
"signal"
],
- "text": "tap.808.kick~",
- "fontname": "Verdana",
- "fontsize": 11.0
+ "patching_rect": [
+ 10.0,
+ 308.0,
+ 160.0,
+ 22.0
+ ],
+ "text": "tap.808.kick~"
}
},
{
"box": {
"id": "obj-13",
- "maxclass": "newobj",
+ "maxclass": "comment",
"numinlets": 1,
- "numoutlets": 5,
- "patching_rect": [
- 20.0,
- 218.0,
- 120.0,
- 20.0
- ],
+ "numoutlets": 1,
"outlettype": [
- "",
- "",
- "",
- "",
""
],
- "text": "sel 1 5 9 13",
- "fontname": "Verdana",
- "fontsize": 11.0
+ "patching_rect": [
+ 180.0,
+ 230.0,
+ 150.0,
+ 20.0
+ ],
+ "text": "snare"
}
},
{
"box": {
"id": "obj-14",
- "maxclass": "message",
+ "maxclass": "newobj",
"numinlets": 1,
"numoutlets": 1,
- "patching_rect": [
- 20.0,
- 246.0,
- 94.0,
- 20.0
- ],
"outlettype": [
""
],
- "text": "trigger 1.",
- "fontname": "Verdana",
- "fontsize": 11.0
+ "patching_rect": [
+ 180.0,
+ 254.0,
+ 160.0,
+ 22.0
+ ],
+ "text": "loadmess velocities 0 0 0 0 0.9 0 0 0 0 0 0 0 0.9 0 0 0"
}
},
{
"box": {
"id": "obj-15",
- "maxclass": "message",
+ "maxclass": "newobj",
"numinlets": 1,
- "numoutlets": 1,
- "patching_rect": [
- 20.0,
- 274.0,
- 94.0,
- 20.0
- ],
+ "numoutlets": 2,
"outlettype": [
+ "signal",
""
],
- "text": "trigger 0.75",
- "fontname": "Verdana",
- "fontsize": 11.0
+ "patching_rect": [
+ 180.0,
+ 278.0,
+ 150.0,
+ 22.0
+ ],
+ "text": "tap.808.seq~"
}
},
{
"box": {
"id": "obj-16",
- "maxclass": "comment",
+ "maxclass": "newobj",
"numinlets": 1,
- "numoutlets": 0,
+ "numoutlets": 1,
+ "outlettype": [
+ "signal"
+ ],
"patching_rect": [
- 154.0,
- 196.0,
- 120.0,
- 18.0
+ 180.0,
+ 308.0,
+ 160.0,
+ 22.0
],
- "text": "snare",
- "fontname": "Verdana",
- "fontsize": 11.0,
- "fontface": 1
+ "text": "tap.808.snare~"
}
},
{
"box": {
"id": "obj-17",
- "maxclass": "newobj",
+ "maxclass": "comment",
"numinlets": 1,
"numoutlets": 1,
- "patching_rect": [
- 154.0,
- 372.0,
- 108.0,
- 21.0
- ],
"outlettype": [
- "signal"
+ ""
+ ],
+ "patching_rect": [
+ 350.0,
+ 230.0,
+ 150.0,
+ 20.0
],
- "text": "tap.808.snare~",
- "fontname": "Verdana",
- "fontsize": 11.0
+ "text": "clap"
}
},
{
@@ -353,79 +340,72 @@
"id": "obj-18",
"maxclass": "newobj",
"numinlets": 1,
- "numoutlets": 3,
- "patching_rect": [
- 154.0,
- 218.0,
- 120.0,
- 20.0
- ],
+ "numoutlets": 1,
"outlettype": [
- "",
- "",
""
],
- "text": "sel 5 13",
- "fontname": "Verdana",
- "fontsize": 11.0
+ "patching_rect": [
+ 350.0,
+ 254.0,
+ 160.0,
+ 22.0
+ ],
+ "text": "loadmess velocities 0 0 0 0 0 0 0 0 0 0 0 0 1. 0 0 0"
}
},
{
"box": {
"id": "obj-19",
- "maxclass": "message",
+ "maxclass": "newobj",
"numinlets": 1,
- "numoutlets": 1,
- "patching_rect": [
- 154.0,
- 246.0,
- 94.0,
- 20.0
- ],
+ "numoutlets": 2,
"outlettype": [
+ "signal",
""
],
- "text": "trigger 0.9",
- "fontname": "Verdana",
- "fontsize": 11.0
+ "patching_rect": [
+ 350.0,
+ 278.0,
+ 150.0,
+ 22.0
+ ],
+ "text": "tap.808.seq~"
}
},
{
"box": {
"id": "obj-20",
- "maxclass": "comment",
+ "maxclass": "newobj",
"numinlets": 1,
- "numoutlets": 0,
+ "numoutlets": 1,
+ "outlettype": [
+ "signal"
+ ],
"patching_rect": [
- 288.0,
- 196.0,
- 120.0,
- 18.0
+ 350.0,
+ 308.0,
+ 160.0,
+ 22.0
],
- "text": "clap",
- "fontname": "Verdana",
- "fontsize": 11.0,
- "fontface": 1
+ "text": "tap.808.clap~"
}
},
{
"box": {
"id": "obj-21",
- "maxclass": "newobj",
+ "maxclass": "comment",
"numinlets": 1,
"numoutlets": 1,
- "patching_rect": [
- 288.0,
- 372.0,
- 101.0,
- 21.0
- ],
"outlettype": [
- "signal"
+ ""
],
- "text": "tap.808.clap~",
- "fontname": "Verdana",
- "fontsize": 11.0
+ "patching_rect": [
+ 520.0,
+ 230.0,
+ 150.0,
+ 20.0
+ ],
+ "text": "hats"
}
},
{
@@ -433,58 +413,54 @@
"id": "obj-22",
"maxclass": "newobj",
"numinlets": 1,
- "numoutlets": 2,
- "patching_rect": [
- 288.0,
- 218.0,
- 120.0,
- 20.0
- ],
+ "numoutlets": 1,
"outlettype": [
- "",
""
],
- "text": "sel 13",
- "fontname": "Verdana",
- "fontsize": 11.0
+ "patching_rect": [
+ 520.0,
+ 254.0,
+ 160.0,
+ 22.0
+ ],
+ "text": "loadmess velocities 0.6 0 0.6 0 0.6 0 0 0 0.6 0 0.6 0 0.6 0 0 0"
}
},
{
"box": {
"id": "obj-23",
- "maxclass": "message",
+ "maxclass": "newobj",
"numinlets": 1,
- "numoutlets": 1,
- "patching_rect": [
- 288.0,
- 246.0,
- 94.0,
- 20.0
- ],
+ "numoutlets": 2,
"outlettype": [
+ "signal",
""
],
- "text": "trigger 1.",
- "fontname": "Verdana",
- "fontsize": 11.0
+ "patching_rect": [
+ 520.0,
+ 278.0,
+ 150.0,
+ 22.0
+ ],
+ "text": "tap.808.seq~"
}
},
{
"box": {
"id": "obj-24",
- "maxclass": "comment",
- "numinlets": 1,
- "numoutlets": 0,
+ "maxclass": "newobj",
+ "numinlets": 2,
+ "numoutlets": 1,
+ "outlettype": [
+ "signal"
+ ],
"patching_rect": [
- 422.0,
- 196.0,
- 120.0,
- 18.0
+ 520.0,
+ 308.0,
+ 160.0,
+ 22.0
],
- "text": "hats",
- "fontname": "Verdana",
- "fontsize": 11.0,
- "fontface": 1
+ "text": "tap.808.hat~"
}
},
{
@@ -493,18 +469,16 @@
"maxclass": "newobj",
"numinlets": 1,
"numoutlets": 1,
- "patching_rect": [
- 422.0,
- 372.0,
- 96.0,
- 21.0
- ],
"outlettype": [
- "signal"
+ ""
+ ],
+ "patching_rect": [
+ 520.0,
+ 350.0,
+ 160.0,
+ 22.0
],
- "text": "tap.808.hat~",
- "fontname": "Verdana",
- "fontsize": 11.0
+ "text": "loadmess velocities 0 0 0 0 0 0 0.8 0 0 0 0 0 0 0 0.8 0"
}
},
{
@@ -512,45 +486,36 @@
"id": "obj-26",
"maxclass": "newobj",
"numinlets": 1,
- "numoutlets": 7,
- "patching_rect": [
- 422.0,
- 218.0,
- 120.0,
- 20.0
- ],
+ "numoutlets": 2,
"outlettype": [
- "",
- "",
- "",
- "",
- "",
- "",
+ "signal",
""
],
- "text": "sel 1 3 5 9 11 13",
- "fontname": "Verdana",
- "fontsize": 11.0
+ "patching_rect": [
+ 520.0,
+ 374.0,
+ 150.0,
+ 22.0
+ ],
+ "text": "tap.808.seq~"
}
},
{
"box": {
"id": "obj-27",
- "maxclass": "message",
+ "maxclass": "comment",
"numinlets": 1,
"numoutlets": 1,
- "patching_rect": [
- 422.0,
- 246.0,
- 94.0,
- 20.0
- ],
"outlettype": [
""
],
- "text": "trigger 0.6",
- "fontname": "Verdana",
- "fontsize": 11.0
+ "patching_rect": [
+ 690.0,
+ 230.0,
+ 150.0,
+ 20.0
+ ],
+ "text": "cymbal"
}
},
{
@@ -558,1061 +523,723 @@
"id": "obj-28",
"maxclass": "newobj",
"numinlets": 1,
- "numoutlets": 3,
- "patching_rect": [
- 422.0,
- 274.0,
- 120.0,
- 20.0
- ],
+ "numoutlets": 1,
"outlettype": [
- "",
- "",
""
],
- "text": "sel 7 15",
- "fontname": "Verdana",
- "fontsize": 11.0
+ "patching_rect": [
+ 690.0,
+ 254.0,
+ 160.0,
+ 22.0
+ ],
+ "text": "loadmess velocities 0.5 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0"
}
},
{
"box": {
"id": "obj-29",
- "maxclass": "message",
+ "maxclass": "newobj",
"numinlets": 1,
- "numoutlets": 1,
- "patching_rect": [
- 422.0,
- 302.0,
- 94.0,
- 20.0
- ],
+ "numoutlets": 2,
"outlettype": [
+ "signal",
""
],
- "text": "open 0.8",
- "fontname": "Verdana",
- "fontsize": 11.0
- }
- },
- {
- "box": {
- "id": "obj-30",
- "maxclass": "comment",
- "numinlets": 1,
- "numoutlets": 0,
"patching_rect": [
- 556.0,
- 196.0,
- 120.0,
- 18.0
+ 690.0,
+ 278.0,
+ 150.0,
+ 22.0
],
- "text": "cymbal",
- "fontname": "Verdana",
- "fontsize": 11.0,
- "fontface": 1
+ "text": "tap.808.seq~"
}
},
{
"box": {
- "id": "obj-31",
+ "id": "obj-30",
"maxclass": "newobj",
"numinlets": 1,
"numoutlets": 1,
- "patching_rect": [
- 556.0,
- 372.0,
- 192.0,
- 21.0
- ],
"outlettype": [
"signal"
],
- "text": "tap.808.cymbal~ @decay 0.5",
- "fontname": "Verdana",
- "fontsize": 11.0
- }
- },
- {
- "box": {
- "id": "obj-32",
- "maxclass": "newobj",
- "numinlets": 1,
- "numoutlets": 2,
"patching_rect": [
- 556.0,
- 218.0,
- 120.0,
- 20.0
- ],
- "outlettype": [
- "",
- ""
+ 690.0,
+ 308.0,
+ 160.0,
+ 22.0
],
- "text": "sel 1",
- "fontname": "Verdana",
- "fontsize": 11.0
+ "text": "tap.808.cymbal~ @decay 0.5"
}
},
{
"box": {
- "id": "obj-33",
- "maxclass": "message",
+ "id": "obj-31",
+ "maxclass": "comment",
"numinlets": 1,
"numoutlets": 1,
- "patching_rect": [
- 556.0,
- 246.0,
- 94.0,
- 20.0
- ],
"outlettype": [
""
],
- "text": "trigger 0.5",
- "fontname": "Verdana",
- "fontsize": 11.0
- }
- },
- {
- "box": {
- "id": "obj-34",
- "maxclass": "comment",
- "numinlets": 1,
- "numoutlets": 0,
"patching_rect": [
- 690.0,
- 196.0,
- 120.0,
- 18.0
+ 10.0,
+ 440.0,
+ 150.0,
+ 20.0
],
- "text": "cowbell",
- "fontname": "Verdana",
- "fontsize": 11.0,
- "fontface": 1
+ "text": "cowbell"
}
},
{
"box": {
- "id": "obj-35",
+ "id": "obj-32",
"maxclass": "newobj",
"numinlets": 1,
"numoutlets": 1,
- "patching_rect": [
- 690.0,
- 372.0,
- 122.0,
- 21.0
- ],
"outlettype": [
- "signal"
+ ""
],
- "text": "tap.808.cowbell~",
- "fontname": "Verdana",
- "fontsize": 11.0
- }
- },
- {
- "box": {
- "id": "obj-36",
- "maxclass": "newobj",
- "numinlets": 1,
- "numoutlets": 2,
"patching_rect": [
- 690.0,
- 218.0,
- 120.0,
- 20.0
- ],
- "outlettype": [
- "",
- ""
+ 10.0,
+ 464.0,
+ 160.0,
+ 22.0
],
- "text": "sel 11",
- "fontname": "Verdana",
- "fontsize": 11.0
+ "text": "loadmess velocities 0 0 0 0 0 0 0 0 0 0 0.7 0 0 0 0 0"
}
},
{
"box": {
- "id": "obj-37",
- "maxclass": "message",
+ "id": "obj-33",
+ "maxclass": "newobj",
"numinlets": 1,
- "numoutlets": 1,
- "patching_rect": [
- 690.0,
- 246.0,
- 94.0,
- 20.0
- ],
+ "numoutlets": 2,
"outlettype": [
+ "signal",
""
],
- "text": "trigger 0.7",
- "fontname": "Verdana",
- "fontsize": 11.0
- }
- },
- {
- "box": {
- "id": "obj-38",
- "maxclass": "comment",
- "numinlets": 1,
- "numoutlets": 0,
"patching_rect": [
- 824.0,
- 196.0,
- 120.0,
- 18.0
+ 10.0,
+ 488.0,
+ 150.0,
+ 22.0
],
- "text": "tom",
- "fontname": "Verdana",
- "fontsize": 11.0,
- "fontface": 1
+ "text": "tap.808.seq~"
}
},
{
"box": {
- "id": "obj-39",
+ "id": "obj-34",
"maxclass": "newobj",
"numinlets": 1,
"numoutlets": 1,
- "patching_rect": [
- 824.0,
- 372.0,
- 150.0,
- 21.0
- ],
"outlettype": [
"signal"
],
- "text": "tap.808.tom~ @size 1",
- "fontname": "Verdana",
- "fontsize": 11.0
- }
- },
- {
- "box": {
- "id": "obj-40",
- "maxclass": "newobj",
- "numinlets": 1,
- "numoutlets": 2,
"patching_rect": [
- 824.0,
- 218.0,
- 120.0,
- 20.0
+ 10.0,
+ 518.0,
+ 160.0,
+ 22.0
],
- "outlettype": [
- "",
- ""
- ],
- "text": "sel 15",
- "fontname": "Verdana",
- "fontsize": 11.0
+ "text": "tap.808.cowbell~"
}
},
{
"box": {
- "id": "obj-41",
- "maxclass": "message",
+ "id": "obj-35",
+ "maxclass": "comment",
"numinlets": 1,
"numoutlets": 1,
- "patching_rect": [
- 824.0,
- 246.0,
- 94.0,
- 20.0
- ],
"outlettype": [
""
],
- "text": "trigger 0.9",
- "fontname": "Verdana",
- "fontsize": 11.0
- }
- },
- {
- "box": {
- "id": "obj-42",
- "maxclass": "comment",
- "numinlets": 1,
- "numoutlets": 0,
"patching_rect": [
- 958.0,
- 196.0,
- 120.0,
- 18.0
+ 180.0,
+ 440.0,
+ 150.0,
+ 20.0
],
- "text": "rim",
- "fontname": "Verdana",
- "fontsize": 11.0,
- "fontface": 1
+ "text": "tom"
}
},
{
"box": {
- "id": "obj-43",
+ "id": "obj-36",
"maxclass": "newobj",
"numinlets": 1,
"numoutlets": 1,
- "patching_rect": [
- 958.0,
- 372.0,
- 96.0,
- 21.0
- ],
"outlettype": [
- "signal"
+ ""
+ ],
+ "patching_rect": [
+ 180.0,
+ 464.0,
+ 160.0,
+ 22.0
],
- "text": "tap.808.rim~",
- "fontname": "Verdana",
- "fontsize": 11.0
+ "text": "loadmess velocities 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.9 0"
}
},
{
"box": {
- "id": "obj-44",
+ "id": "obj-37",
"maxclass": "newobj",
"numinlets": 1,
"numoutlets": 2,
- "patching_rect": [
- 958.0,
- 218.0,
- 120.0,
- 20.0
- ],
"outlettype": [
- "",
+ "signal",
""
],
- "text": "sel 8",
- "fontname": "Verdana",
- "fontsize": 11.0
- }
- },
- {
- "box": {
- "id": "obj-45",
- "maxclass": "message",
- "numinlets": 1,
- "numoutlets": 1,
"patching_rect": [
- 958.0,
- 246.0,
- 94.0,
- 20.0
- ],
- "outlettype": [
- ""
- ],
- "text": "trigger 0.6",
- "fontname": "Verdana",
- "fontsize": 11.0
- }
- }
- ],
- "lines": [
- {
- "patchline": {
- "source": [
- "obj-4",
- 0
- ],
- "destination": [
- "obj-5",
- 0
- ],
- "hidden": 0,
- "disabled": 0
- }
- },
- {
- "patchline": {
- "source": [
- "obj-5",
- 0
- ],
- "destination": [
- "obj-6",
- 0
- ],
- "hidden": 0,
- "disabled": 0
- }
- },
- {
- "patchline": {
- "source": [
- "obj-6",
- 0
- ],
- "destination": [
- "obj-13",
- 0
- ],
- "hidden": 0,
- "disabled": 0
- }
- },
- {
- "patchline": {
- "source": [
- "obj-13",
- 0
- ],
- "destination": [
- "obj-14",
- 0
- ],
- "hidden": 0,
- "disabled": 0
- }
- },
- {
- "patchline": {
- "source": [
- "obj-14",
- 0
- ],
- "destination": [
- "obj-12",
- 0
- ],
- "hidden": 0,
- "disabled": 0
- }
- },
- {
- "patchline": {
- "source": [
- "obj-13",
- 1
- ],
- "destination": [
- "obj-15",
- 0
- ],
- "hidden": 0,
- "disabled": 0
- }
- },
- {
- "patchline": {
- "source": [
- "obj-13",
- 2
- ],
- "destination": [
- "obj-15",
- 0
- ],
- "hidden": 0,
- "disabled": 0
- }
- },
- {
- "patchline": {
- "source": [
- "obj-13",
- 3
- ],
- "destination": [
- "obj-15",
- 0
+ 180.0,
+ 488.0,
+ 150.0,
+ 22.0
],
- "hidden": 0,
- "disabled": 0
+ "text": "tap.808.seq~"
}
},
{
- "patchline": {
- "source": [
- "obj-15",
- 0
+ "box": {
+ "id": "obj-38",
+ "maxclass": "newobj",
+ "numinlets": 1,
+ "numoutlets": 1,
+ "outlettype": [
+ "signal"
],
- "destination": [
- "obj-12",
- 0
+ "patching_rect": [
+ 180.0,
+ 518.0,
+ 160.0,
+ 22.0
],
- "hidden": 0,
- "disabled": 0
+ "text": "tap.808.tom~ @size 1"
}
},
{
- "patchline": {
- "source": [
- "obj-12",
- 0
+ "box": {
+ "id": "obj-39",
+ "maxclass": "comment",
+ "numinlets": 1,
+ "numoutlets": 1,
+ "outlettype": [
+ ""
],
- "destination": [
- "obj-8",
- 0
+ "patching_rect": [
+ 350.0,
+ 440.0,
+ 150.0,
+ 20.0
],
- "hidden": 0,
- "disabled": 0
+ "text": "rim"
}
},
{
- "patchline": {
- "source": [
- "obj-6",
- 0
+ "box": {
+ "id": "obj-40",
+ "maxclass": "newobj",
+ "numinlets": 1,
+ "numoutlets": 1,
+ "outlettype": [
+ ""
],
- "destination": [
- "obj-18",
- 0
+ "patching_rect": [
+ 350.0,
+ 464.0,
+ 160.0,
+ 22.0
],
- "hidden": 0,
- "disabled": 0
+ "text": "loadmess velocities 0 0 0 0 0 0 0 0.6 0 0 0 0 0 0 0 0"
}
},
{
- "patchline": {
- "source": [
- "obj-18",
- 0
+ "box": {
+ "id": "obj-41",
+ "maxclass": "newobj",
+ "numinlets": 1,
+ "numoutlets": 2,
+ "outlettype": [
+ "signal",
+ ""
],
- "destination": [
- "obj-19",
- 0
+ "patching_rect": [
+ 350.0,
+ 488.0,
+ 150.0,
+ 22.0
],
- "hidden": 0,
- "disabled": 0
+ "text": "tap.808.seq~"
}
},
{
- "patchline": {
- "source": [
- "obj-18",
- 1
+ "box": {
+ "id": "obj-42",
+ "maxclass": "newobj",
+ "numinlets": 1,
+ "numoutlets": 1,
+ "outlettype": [
+ "signal"
],
- "destination": [
- "obj-19",
- 0
+ "patching_rect": [
+ 350.0,
+ 518.0,
+ 160.0,
+ 22.0
],
- "hidden": 0,
- "disabled": 0
+ "text": "tap.808.rim~"
}
- },
+ }
+ ],
+ "lines": [
{
"patchline": {
"source": [
- "obj-19",
+ "obj-6",
0
],
"destination": [
- "obj-17",
+ "obj-7",
0
- ],
- "hidden": 0,
- "disabled": 0
+ ]
}
},
{
"patchline": {
"source": [
- "obj-17",
+ "obj-6",
0
],
"destination": [
- "obj-8",
- 0
- ],
- "hidden": 0,
- "disabled": 0
+ "obj-7",
+ 1
+ ]
}
},
{
"patchline": {
"source": [
- "obj-6",
+ "obj-4",
0
],
"destination": [
- "obj-22",
+ "obj-11",
0
- ],
- "hidden": 0,
- "disabled": 0
+ ]
}
},
{
"patchline": {
"source": [
- "obj-22",
+ "obj-10",
0
],
"destination": [
- "obj-23",
+ "obj-11",
0
- ],
- "hidden": 0,
- "disabled": 0
+ ]
}
},
{
"patchline": {
"source": [
- "obj-23",
+ "obj-11",
0
],
"destination": [
- "obj-21",
+ "obj-12",
0
- ],
- "hidden": 0,
- "disabled": 0
+ ]
}
},
{
"patchline": {
"source": [
- "obj-21",
+ "obj-12",
0
],
"destination": [
- "obj-8",
+ "obj-6",
0
- ],
- "hidden": 0,
- "disabled": 0
+ ]
}
},
{
"patchline": {
"source": [
- "obj-6",
+ "obj-4",
0
],
"destination": [
- "obj-26",
+ "obj-15",
0
- ],
- "hidden": 0,
- "disabled": 0
+ ]
}
},
{
"patchline": {
"source": [
- "obj-26",
+ "obj-14",
0
],
"destination": [
- "obj-27",
+ "obj-15",
0
- ],
- "hidden": 0,
- "disabled": 0
+ ]
}
},
{
"patchline": {
"source": [
- "obj-26",
- 1
+ "obj-15",
+ 0
],
"destination": [
- "obj-27",
+ "obj-16",
0
- ],
- "hidden": 0,
- "disabled": 0
+ ]
}
},
{
"patchline": {
"source": [
- "obj-26",
- 2
+ "obj-16",
+ 0
],
"destination": [
- "obj-27",
+ "obj-6",
0
- ],
- "hidden": 0,
- "disabled": 0
+ ]
}
},
{
"patchline": {
"source": [
- "obj-26",
- 3
+ "obj-4",
+ 0
],
"destination": [
- "obj-27",
+ "obj-19",
0
- ],
- "hidden": 0,
- "disabled": 0
+ ]
}
},
{
"patchline": {
"source": [
- "obj-26",
- 4
+ "obj-18",
+ 0
],
"destination": [
- "obj-27",
+ "obj-19",
0
- ],
- "hidden": 0,
- "disabled": 0
+ ]
}
},
{
"patchline": {
"source": [
- "obj-26",
- 5
+ "obj-19",
+ 0
],
"destination": [
- "obj-27",
+ "obj-20",
0
- ],
- "hidden": 0,
- "disabled": 0
+ ]
}
},
{
"patchline": {
"source": [
- "obj-27",
+ "obj-20",
0
],
"destination": [
- "obj-25",
+ "obj-6",
0
- ],
- "hidden": 0,
- "disabled": 0
+ ]
}
},
{
"patchline": {
"source": [
- "obj-6",
+ "obj-4",
0
],
"destination": [
- "obj-28",
+ "obj-23",
0
- ],
- "hidden": 0,
- "disabled": 0
+ ]
}
},
{
"patchline": {
"source": [
- "obj-28",
+ "obj-22",
0
],
"destination": [
- "obj-29",
+ "obj-23",
0
- ],
- "hidden": 0,
- "disabled": 0
+ ]
}
},
{
"patchline": {
"source": [
- "obj-28",
- 1
+ "obj-23",
+ 0
],
"destination": [
- "obj-29",
+ "obj-24",
0
- ],
- "hidden": 0,
- "disabled": 0
+ ]
}
},
{
"patchline": {
"source": [
- "obj-29",
+ "obj-24",
0
],
"destination": [
- "obj-25",
+ "obj-6",
0
- ],
- "hidden": 0,
- "disabled": 0
+ ]
}
},
{
"patchline": {
"source": [
- "obj-25",
+ "obj-4",
0
],
"destination": [
- "obj-8",
+ "obj-26",
0
- ],
- "hidden": 0,
- "disabled": 0
+ ]
}
},
{
"patchline": {
"source": [
- "obj-6",
+ "obj-25",
0
],
"destination": [
- "obj-32",
+ "obj-26",
0
- ],
- "hidden": 0,
- "disabled": 0
+ ]
}
},
{
"patchline": {
"source": [
- "obj-32",
+ "obj-26",
0
],
"destination": [
- "obj-33",
- 0
- ],
- "hidden": 0,
- "disabled": 0
+ "obj-24",
+ 1
+ ]
}
},
{
"patchline": {
"source": [
- "obj-33",
+ "obj-4",
0
],
"destination": [
- "obj-31",
+ "obj-29",
0
- ],
- "hidden": 0,
- "disabled": 0
+ ]
}
},
{
"patchline": {
"source": [
- "obj-31",
+ "obj-28",
0
],
"destination": [
- "obj-8",
+ "obj-29",
0
- ],
- "hidden": 0,
- "disabled": 0
+ ]
}
},
{
"patchline": {
"source": [
- "obj-6",
+ "obj-29",
0
],
"destination": [
- "obj-36",
+ "obj-30",
0
- ],
- "hidden": 0,
- "disabled": 0
+ ]
}
},
{
"patchline": {
"source": [
- "obj-36",
+ "obj-30",
0
],
"destination": [
- "obj-37",
+ "obj-6",
0
- ],
- "hidden": 0,
- "disabled": 0
+ ]
}
},
{
"patchline": {
"source": [
- "obj-37",
+ "obj-4",
0
],
"destination": [
- "obj-35",
+ "obj-33",
0
- ],
- "hidden": 0,
- "disabled": 0
+ ]
}
},
{
"patchline": {
"source": [
- "obj-35",
+ "obj-32",
0
],
"destination": [
- "obj-8",
+ "obj-33",
0
- ],
- "hidden": 0,
- "disabled": 0
+ ]
}
},
{
"patchline": {
"source": [
- "obj-6",
+ "obj-33",
0
],
"destination": [
- "obj-40",
+ "obj-34",
0
- ],
- "hidden": 0,
- "disabled": 0
+ ]
}
},
{
"patchline": {
"source": [
- "obj-40",
+ "obj-34",
0
],
"destination": [
- "obj-41",
+ "obj-6",
0
- ],
- "hidden": 0,
- "disabled": 0
+ ]
}
},
{
"patchline": {
"source": [
- "obj-41",
+ "obj-4",
0
],
"destination": [
- "obj-39",
+ "obj-37",
0
- ],
- "hidden": 0,
- "disabled": 0
+ ]
}
},
{
"patchline": {
"source": [
- "obj-39",
+ "obj-36",
0
],
"destination": [
- "obj-8",
+ "obj-37",
0
- ],
- "hidden": 0,
- "disabled": 0
+ ]
}
},
{
"patchline": {
"source": [
- "obj-6",
+ "obj-37",
0
],
"destination": [
- "obj-44",
+ "obj-38",
0
- ],
- "hidden": 0,
- "disabled": 0
+ ]
}
},
{
"patchline": {
"source": [
- "obj-44",
+ "obj-38",
0
],
"destination": [
- "obj-45",
+ "obj-6",
0
- ],
- "hidden": 0,
- "disabled": 0
+ ]
}
},
{
"patchline": {
"source": [
- "obj-45",
+ "obj-4",
0
],
"destination": [
- "obj-43",
+ "obj-41",
0
- ],
- "hidden": 0,
- "disabled": 0
+ ]
}
},
{
"patchline": {
"source": [
- "obj-43",
+ "obj-40",
0
],
"destination": [
- "obj-8",
+ "obj-41",
0
- ],
- "hidden": 0,
- "disabled": 0
+ ]
}
},
{
"patchline": {
"source": [
- "obj-8",
+ "obj-41",
0
],
"destination": [
- "obj-9",
+ "obj-42",
0
- ],
- "hidden": 0,
- "disabled": 0
+ ]
}
},
{
"patchline": {
"source": [
- "obj-8",
+ "obj-42",
0
],
"destination": [
- "obj-9",
- 1
- ],
- "hidden": 0,
- "disabled": 0
+ "obj-6",
+ 0
+ ]
}
}
- ]
+ ],
+ "dependency_cache": [],
+ "autosave": 0
}
}
diff --git a/help/tap.808.seq~.maxhelp b/help/tap.808.seq~.maxhelp
new file mode 100644
index 00000000..f39a291c
--- /dev/null
+++ b/help/tap.808.seq~.maxhelp
@@ -0,0 +1,661 @@
+{
+ "patcher": {
+ "fileversion": 1,
+ "appversion": {
+ "major": 8,
+ "minor": 6,
+ "revision": 2,
+ "architecture": "x64",
+ "modernui": 1
+ },
+ "classnamespace": "box",
+ "rect": [
+ 120.0,
+ 80.0,
+ 900.0,
+ 720.0
+ ],
+ "bglocked": 0,
+ "openinpresentation": 0,
+ "default_fontsize": 11.0,
+ "default_fontface": 0,
+ "default_fontname": "Arial",
+ "gridonopen": 1,
+ "gridsize": [
+ 15.0,
+ 15.0
+ ],
+ "gridsnaponopen": 1,
+ "statusbarvisible": 2,
+ "toolbarvisible": 1,
+ "description": "tap.808.seq~: a phase-clocked drum row driving tap.808.kick~ \u2014 hits/accents, velocities, swing, polymeter, and quantized slot recall.",
+ "boxes": [
+ {
+ "box": {
+ "id": "obj-1",
+ "maxclass": "comment",
+ "numinlets": 1,
+ "numoutlets": 1,
+ "outlettype": [
+ ""
+ ],
+ "patching_rect": [
+ 10.0,
+ 8.0,
+ 300.0,
+ 20.0
+ ],
+ "text": "tap.808.seq~ \u2014 one drum row"
+ }
+ },
+ {
+ "box": {
+ "id": "obj-2",
+ "maxclass": "comment",
+ "numinlets": 1,
+ "numoutlets": 1,
+ "outlettype": [
+ ""
+ ],
+ "patching_rect": [
+ 10.0,
+ 32.0,
+ 860.0,
+ 48.0
+ ],
+ "text": "A phase-clocked step row: phasor~ (one ramp per pattern cycle) in, trigger impulses out \u2014 the amplitude 0..1 is the accent, the tap.808.* contract. Every row on one phasor stays sample-locked; a different @length is polymeter. The right outlet reports the step number for displays."
+ }
+ },
+ {
+ "box": {
+ "id": "obj-3",
+ "maxclass": "newobj",
+ "numinlets": 2,
+ "numoutlets": 1,
+ "outlettype": [
+ "signal"
+ ],
+ "patching_rect": [
+ 10.0,
+ 96.0,
+ 110.0,
+ 22.0
+ ],
+ "text": "phasor~ 0.4845"
+ }
+ },
+ {
+ "box": {
+ "id": "obj-4",
+ "maxclass": "comment",
+ "numinlets": 1,
+ "numoutlets": 1,
+ "outlettype": [
+ ""
+ ],
+ "patching_rect": [
+ 130.0,
+ 96.0,
+ 420.0,
+ 20.0
+ ],
+ "text": "<- one ramp per bar at ~116 BPM; lock it to the transport if you like"
+ }
+ },
+ {
+ "box": {
+ "id": "obj-5",
+ "maxclass": "newobj",
+ "numinlets": 1,
+ "numoutlets": 2,
+ "outlettype": [
+ "signal",
+ ""
+ ],
+ "patching_rect": [
+ 10.0,
+ 300.0,
+ 260.0,
+ 22.0
+ ],
+ "text": "tap.808.seq~ @plain 0.6 @accented 1."
+ }
+ },
+ {
+ "box": {
+ "id": "obj-6",
+ "maxclass": "message",
+ "numinlets": 2,
+ "numoutlets": 1,
+ "outlettype": [
+ ""
+ ],
+ "patching_rect": [
+ 10.0,
+ 140.0,
+ 340.0,
+ 20.0
+ ],
+ "text": "hits 1 0 0 0 1 0 0 0 1 0 0 0 1 0 1 0"
+ }
+ },
+ {
+ "box": {
+ "id": "obj-7",
+ "maxclass": "message",
+ "numinlets": 2,
+ "numoutlets": 1,
+ "outlettype": [
+ ""
+ ],
+ "patching_rect": [
+ 10.0,
+ 168.0,
+ 340.0,
+ 20.0
+ ],
+ "text": "accents 1 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0"
+ }
+ },
+ {
+ "box": {
+ "id": "obj-8",
+ "maxclass": "newobj",
+ "numinlets": 1,
+ "numoutlets": 1,
+ "outlettype": [
+ "bang"
+ ],
+ "patching_rect": [
+ 370.0,
+ 140.0,
+ 70.0,
+ 22.0
+ ],
+ "text": "loadbang"
+ }
+ },
+ {
+ "box": {
+ "id": "obj-9",
+ "maxclass": "comment",
+ "numinlets": 1,
+ "numoutlets": 1,
+ "outlettype": [
+ ""
+ ],
+ "patching_rect": [
+ 10.0,
+ 196.0,
+ 620.0,
+ 20.0
+ ],
+ "text": "hits/accents are the hardware view (mapped onto @plain/@accented); velocities sets arbitrary per-step levels:"
+ }
+ },
+ {
+ "box": {
+ "id": "obj-10",
+ "maxclass": "message",
+ "numinlets": 2,
+ "numoutlets": 1,
+ "outlettype": [
+ ""
+ ],
+ "patching_rect": [
+ 10.0,
+ 222.0,
+ 480.0,
+ 20.0
+ ],
+ "text": "velocities 1. 0 0 0.3 0.8 0 0.4 0 1. 0 0 0.3 0.8 0 0.55 0.3"
+ }
+ },
+ {
+ "box": {
+ "id": "obj-11",
+ "maxclass": "message",
+ "numinlets": 2,
+ "numoutlets": 1,
+ "outlettype": [
+ ""
+ ],
+ "patching_rect": [
+ 360.0,
+ 260.0,
+ 80.0,
+ 20.0
+ ],
+ "text": "swing 0.67"
+ }
+ },
+ {
+ "box": {
+ "id": "obj-12",
+ "maxclass": "comment",
+ "numinlets": 1,
+ "numoutlets": 1,
+ "outlettype": [
+ ""
+ ],
+ "patching_rect": [
+ 450.0,
+ 260.0,
+ 260.0,
+ 20.0
+ ],
+ "text": "<- the classic shuffle (0 = straight, stock)"
+ }
+ },
+ {
+ "box": {
+ "id": "obj-13",
+ "maxclass": "message",
+ "numinlets": 2,
+ "numoutlets": 1,
+ "outlettype": [
+ ""
+ ],
+ "patching_rect": [
+ 360.0,
+ 288.0,
+ 70.0,
+ 20.0
+ ],
+ "text": "length 12"
+ }
+ },
+ {
+ "box": {
+ "id": "obj-14",
+ "maxclass": "comment",
+ "numinlets": 1,
+ "numoutlets": 1,
+ "outlettype": [
+ ""
+ ],
+ "patching_rect": [
+ 440.0,
+ 288.0,
+ 300.0,
+ 20.0
+ ],
+ "text": "<- 12 against another row's 16 = polymeter"
+ }
+ },
+ {
+ "box": {
+ "id": "obj-15",
+ "maxclass": "message",
+ "numinlets": 2,
+ "numoutlets": 1,
+ "outlettype": [
+ ""
+ ],
+ "patching_rect": [
+ 360.0,
+ 316.0,
+ 60.0,
+ 20.0
+ ],
+ "text": "store 2"
+ }
+ },
+ {
+ "box": {
+ "id": "obj-16",
+ "maxclass": "message",
+ "numinlets": 2,
+ "numoutlets": 1,
+ "outlettype": [
+ ""
+ ],
+ "patching_rect": [
+ 430.0,
+ 316.0,
+ 60.0,
+ 20.0
+ ],
+ "text": "recall 2"
+ }
+ },
+ {
+ "box": {
+ "id": "obj-17",
+ "maxclass": "message",
+ "numinlets": 2,
+ "numoutlets": 1,
+ "outlettype": [
+ ""
+ ],
+ "patching_rect": [
+ 500.0,
+ 316.0,
+ 60.0,
+ 20.0
+ ],
+ "text": "recall 1"
+ }
+ },
+ {
+ "box": {
+ "id": "obj-18",
+ "maxclass": "comment",
+ "numinlets": 1,
+ "numoutlets": 1,
+ "outlettype": [
+ ""
+ ],
+ "patching_rect": [
+ 570.0,
+ 316.0,
+ 320.0,
+ 20.0
+ ],
+ "text": "<- slots swap at the next pattern top (@quantize) \u2014 the A/B / fill move"
+ }
+ },
+ {
+ "box": {
+ "id": "obj-19",
+ "maxclass": "newobj",
+ "numinlets": 1,
+ "numoutlets": 1,
+ "outlettype": [
+ "signal"
+ ],
+ "patching_rect": [
+ 10.0,
+ 360.0,
+ 110.0,
+ 22.0
+ ],
+ "text": "tap.808.kick~"
+ }
+ },
+ {
+ "box": {
+ "id": "obj-20",
+ "maxclass": "comment",
+ "numinlets": 1,
+ "numoutlets": 1,
+ "outlettype": [
+ ""
+ ],
+ "patching_rect": [
+ 130.0,
+ 360.0,
+ 380.0,
+ 20.0
+ ],
+ "text": "<- or any tap.808.* voice, tap.adsr~ (@pulse widens the impulse), ..."
+ }
+ },
+ {
+ "box": {
+ "id": "obj-21",
+ "maxclass": "number",
+ "numinlets": 1,
+ "numoutlets": 2,
+ "outlettype": [
+ "",
+ "bang"
+ ],
+ "patching_rect": [
+ 290.0,
+ 300.0,
+ 50.0,
+ 22.0
+ ]
+ }
+ },
+ {
+ "box": {
+ "id": "obj-22",
+ "maxclass": "comment",
+ "numinlets": 1,
+ "numoutlets": 1,
+ "outlettype": [
+ ""
+ ],
+ "patching_rect": [
+ 345.0,
+ 300.0,
+ 10.0,
+ 20.0
+ ],
+ "text": ""
+ }
+ },
+ {
+ "box": {
+ "id": "obj-23",
+ "maxclass": "newobj",
+ "numinlets": 2,
+ "numoutlets": 1,
+ "outlettype": [
+ "signal"
+ ],
+ "patching_rect": [
+ 10.0,
+ 402.0,
+ 40.0,
+ 22.0
+ ],
+ "text": "*~ 0.8"
+ }
+ },
+ {
+ "box": {
+ "id": "obj-24",
+ "maxclass": "ezdac~",
+ "numinlets": 2,
+ "numoutlets": 0,
+ "outlettype": [],
+ "patching_rect": [
+ 10.0,
+ 444.0,
+ 45.0,
+ 45.0
+ ]
+ }
+ }
+ ],
+ "lines": [
+ {
+ "patchline": {
+ "source": [
+ "obj-3",
+ 0
+ ],
+ "destination": [
+ "obj-5",
+ 0
+ ]
+ }
+ },
+ {
+ "patchline": {
+ "source": [
+ "obj-8",
+ 0
+ ],
+ "destination": [
+ "obj-6",
+ 0
+ ]
+ }
+ },
+ {
+ "patchline": {
+ "source": [
+ "obj-8",
+ 0
+ ],
+ "destination": [
+ "obj-7",
+ 0
+ ]
+ }
+ },
+ {
+ "patchline": {
+ "source": [
+ "obj-6",
+ 0
+ ],
+ "destination": [
+ "obj-5",
+ 0
+ ]
+ }
+ },
+ {
+ "patchline": {
+ "source": [
+ "obj-7",
+ 0
+ ],
+ "destination": [
+ "obj-5",
+ 0
+ ]
+ }
+ },
+ {
+ "patchline": {
+ "source": [
+ "obj-10",
+ 0
+ ],
+ "destination": [
+ "obj-5",
+ 0
+ ]
+ }
+ },
+ {
+ "patchline": {
+ "source": [
+ "obj-11",
+ 0
+ ],
+ "destination": [
+ "obj-5",
+ 0
+ ]
+ }
+ },
+ {
+ "patchline": {
+ "source": [
+ "obj-13",
+ 0
+ ],
+ "destination": [
+ "obj-5",
+ 0
+ ]
+ }
+ },
+ {
+ "patchline": {
+ "source": [
+ "obj-15",
+ 0
+ ],
+ "destination": [
+ "obj-5",
+ 0
+ ]
+ }
+ },
+ {
+ "patchline": {
+ "source": [
+ "obj-16",
+ 0
+ ],
+ "destination": [
+ "obj-5",
+ 0
+ ]
+ }
+ },
+ {
+ "patchline": {
+ "source": [
+ "obj-17",
+ 0
+ ],
+ "destination": [
+ "obj-5",
+ 0
+ ]
+ }
+ },
+ {
+ "patchline": {
+ "source": [
+ "obj-5",
+ 0
+ ],
+ "destination": [
+ "obj-19",
+ 0
+ ]
+ }
+ },
+ {
+ "patchline": {
+ "source": [
+ "obj-5",
+ 1
+ ],
+ "destination": [
+ "obj-21",
+ 0
+ ]
+ }
+ },
+ {
+ "patchline": {
+ "source": [
+ "obj-19",
+ 0
+ ],
+ "destination": [
+ "obj-23",
+ 0
+ ]
+ }
+ },
+ {
+ "patchline": {
+ "source": [
+ "obj-23",
+ 0
+ ],
+ "destination": [
+ "obj-24",
+ 0
+ ]
+ }
+ },
+ {
+ "patchline": {
+ "source": [
+ "obj-23",
+ 0
+ ],
+ "destination": [
+ "obj-24",
+ 1
+ ]
+ }
+ }
+ ],
+ "dependency_cache": [],
+ "autosave": 0
+ }
+}
diff --git a/plans/tap.303.md b/plans/tap.303.md
index 49fa1ada..beccecbe 100644
--- a/plans/tap.303.md
+++ b/plans/tap.303.md
@@ -266,6 +266,10 @@ each gated by A/B evidence (the 808 plan's go/no-go discipline) — all resolved
## 6. Phase 3 — the sequencer (own object, deferred)
+> ✅ **Designed and shipped via `plans/tap.seq.md`** (2026-07-18): a shared kernel step
+> engine (`step_seq.h`) with `tap.303.seq~` and `tap.808.seq~` as thin per-family
+> emitters, phase-clocked from `phasor~`. `tap.303.seq~` is in the package.
+
`tap.303.seq~`: a signal-clocked step sequencer emitting **exactly the voice's inlet
contract** — a pitch signal and a gate signal with amplitude-encoded accent, with
slide realized as gate-hold + stepped pitch (the voice's legato detection does the
diff --git a/plans/tap.808.md b/plans/tap.808.md
index d202bbf3..fb586bc4 100644
--- a/plans/tap.808.md
+++ b/plans/tap.808.md
@@ -255,6 +255,10 @@ tolerances — using **wave digital filters** over the actual schematic.
## 6. Phase 3 — the sequencer (potential, own objects)
+> ✅ **Designed and shipped via `plans/tap.seq.md`** (2026-07-18): option (b) below,
+> unified with the 303's emitter over one shared kernel engine (`step_seq.h`).
+> `tap.808.seq~` is in the package; the §8 Q5 decision is resolved by that plan.
+
The hardware's other half: 16 steps × A/B pattern halves, basic/fill patterns with
chaining, 12 instrument rows + the accent row, and "pre-scale" step resolutions
(including triplet grids). Deliberately **deferred until phase 1 has shipped** and the
diff --git a/plans/tap.seq.md b/plans/tap.seq.md
new file mode 100644
index 00000000..d0769894
--- /dev/null
+++ b/plans/tap.seq.md
@@ -0,0 +1,316 @@
+# Plan — `tap.808.seq~` / `tap.303.seq~` (the shared step-sequencer engine)
+
+> Status: **shipped 2026-07-18** — drafted, §6-approved by the author, and implemented
+> the same day (slices 0–3; see the REVIVAL.md §7 entry). The engine lives in the kernel
+> repo as `step_seq.h` (`taptools::seq`: the phase→step engine + `trigger_row` +
+> `note_row`, 19 test scenarios including the tb303-voice pairing); the wrappers,
+> unittests, maxrefs, help patchers, and maxtest starters are here, and
+> `help/tap.808.maxhelp` is rebuilt on real rows. Slice-0 pins: 808 trigger levels
+> plain 0.01 / accented 0.5 (the 4–14 V bus mapping), 303 gate duty 0.5 (Open303
+> `AcidPattern::stepLength`), slide flag on the *target* step (the package `note`
+> convention; the hardware's source-note storage documented as a divergence in the
+> kernel header). The non-blocking recommendations (§6 Q4–Q7) all shipped as
+> recommended. The kernel repo also carries **`notebooks/step_seq.ipynb`** — the
+> executed, asserted verification notebook (C ABI extended with the rows + a minimal
+> kick), ending in four bars of acid + kick rendered from the shipping voices off one
+> phasor ramp. Remaining: **runtime validation in Max** (the maxtest starters want
+> extending on a licensed install), per the package-wide gate.
+>
+> Original preconditions, for the record: both voice families shipped (808 slices 0–5 +
+> §7.2 calibration; 303 phases 1–2 + the approved WDF no-go), the contracts proven in
+> their family patchers, and the `help/tap.303~-pattern.maxpat` dry run.
+
+## 0. The headline answer, up front
+
+**Can the 303 and 808 sequencers be shared? Yes — at the kernel level, not the object
+level.** Everything hard is common: deriving the current step from a signal clock with
+sample accuracy, swing, pattern data storage and slots, run/wrap logic, determinism. What
+differs is only the per-step *payload* and how a step is *emitted* as signal:
+
+| | `tap.808.seq~` (one drum row) | `tap.303.seq~` (one bass line) |
+|---|---|---|
+| Per-step data | on/off + accent (velocity) | pitch, gate/rest, accent flag, slide flag |
+| Output | **1 signal outlet**: trigger impulses, edge amplitude 0..1 = accent — the `tap.808.*` trigger contract verbatim | **2 signal outlets**: pitch (MIDI note number) + gate with amplitude 1 (plain) / 2 (accented); slide = gate held across the boundary while pitch steps — the `tap.303~` inlet contract verbatim |
+| Consumes | any `tap.808.*` voice, `tap.adsr~`, envelopes, anything edge-triggered | `tap.303~` (and any future melodic voice honoring the package contract) |
+
+So: **one kernel header, `step_seq.h` (`taptools::seq`) — a clock/step engine plus two
+thin emitters — and two thin Min wrappers**, one per family. Sharing between kernels is
+exactly what the kernel repo is for; separate Max objects keep each family's surface,
+help patchers, and `RELATED` graph coherent, and the two objects' outlet counts and
+amplitude conventions genuinely differ. This is option **(b)** from `plans/tap.808.md`
+§6 (row decomposition, recommended there), unified with the `tap.303.seq~` emitter the
+303 plan §6 specified.
+
+Explicitly rejected:
+
+- **One generic `tap.seq~` object serving both.** The two contracts differ in arity and
+ in accent encoding (808: absolute 0..1; 303: 1 + depth); a merged object needs mode
+ switches that change its outlet count — un-Max-like. (The engine *is* generic; a
+ generic third wrapper can always be added later as another thin emitter. Deferred,
+ §8 Q5.)
+- **A monolithic multi-row hardware-mirror** — option (c) in the 808 plan, rejected
+ there for good reasons (12+ signal outlets, duplicates Max patching).
+- **Recreating the 1981 entry workflows** — the 808's basic/fill chaining panel logic
+ and the 303's pitch-mode/time-mode data entry. We keep the *data model* (steps with
+ flags), not the data-entry UX. Both plans already pinned this.
+- **An event-domain (scheduler-clocked) sequencer** — option (a): trigger timing would
+ quantize to the scheduler, a mismatch with the voices' sample-accurate signal inlets.
+ The `note`-message dry-run patcher already demonstrates the event-domain idiom for
+ anyone who wants it.
+
+## 1. Why this shape
+
+- **Both §6 sections demanded coordination**: "if both land, they should share the
+ clocking idiom, and a 303 row driving a `tap.808.*` voice (or vice versa) should Just
+ Work." One engine makes the shared idiom structural rather than aspirational.
+- **The voice contracts were designed for this.** The 808 voices take a signal edge
+ whose amplitude is the accent (the shared 4–14 V trigger bus); `tap.303~` takes a
+ pitch signal + a gate signal with amplitude-as-accent and legato-as-slide. The
+ sequencer rows emit exactly those signals — no adapter objects, no new conventions.
+- **Rows are useful beyond the families**: a trigger row drives `tap.adsr~`, `click~`
+ chains, anything with an edge input; a pitch/gate pair drives any future melodic
+ voice honoring the package contract (`plans/tap.303.md` §8 decision 2 made it
+ package-wide).
+- **Cross-family patching, honestly stated**: a 303 row's gate edge (amplitude ≥ 1)
+ triggers any 808 voice — the voice clamps accent to 0..1, so it fires at full accent;
+ musically useful (a bass line doubling a kick). The reverse (an 808 row's one-sample
+ impulse into the 303 gate) triggers but gives a one-sample note — the 303 needs held
+ gates for note length, so that direction is documented as not the idiom.
+
+## 2. Reference hardware — the sequencer halves
+
+Facts from the owner's manuals / service notes already fetched for the voice phases;
+remaining timing constants (gate duty, trigger levels) pinned at slice 0 with provenance
+comments, per house rule.
+
+- **TR-808**: 16 steps × A/B pattern halves, 12 instrument rows plus the **accent row**
+ (a shared bus raising every sounding voice's trigger voltage on accented steps, scaled
+ by the AC level knob), basic/fill patterns with chaining, and "pre-scale" step
+ resolutions including triplet grids. No swing on the hardware.
+- **TB-303**: 16-step patterns; per step: note, gate/rest, **accent flag**, **slide
+ flag**, octave up/down transposition. Slide = the sequencer holds the gate across the
+ step boundary while the pitch CV glides (~60 ms RC — already in the voice); accent =
+ a hotter CV into the coupled accent circuitry (already in the voice). The famously
+ opaque entry workflow is not part of the data model.
+
+Mapping to the design: the accent row and AC knob become per-row accent data plus the
+voices' own accent handling (the bus is a patch cord now); A/B halves, fills, chaining,
+and pre-scale become **pattern slots + boundary-quantized recall + the clock rate**
+(§3.3); the 303's octave up/down flags are subsumed by pitch-as-MIDI-float (plus a live
+`transpose` message, which is what the hardware's transpose actually did).
+
+## 3. Design
+
+### 3.1 The clocking idiom (the load-bearing shared decision)
+
+**Phase-driven: one 0..1 ramp per pattern cycle, from `phasor~`.** The row derives the
+current step as `floor(phase × length)` (swing warps the boundaries, §3.3), detecting
+boundary crossings per sample.
+
+Why phase, not a pulse clock:
+
+- **Sample-accurate and tempo-agnostic** — `phasor~ @lock 1` (or `phasor~` off the
+ transport) gives bar-locked ramps for free; the row never owns tempo.
+- **Every row locked to one phasor stays phase-coherent forever** — no drift, no
+ reset choreography; mute/unmute a row and it re-enters in place. This is the
+ multi-row story: twelve rows = twelve objects fed by one patch cord.
+- **Different `length` values off the same ramp are polymeter for free**; a
+ `length 12` row against `length 16` rows is the 808's triplet pre-scale
+ (and more than the hardware allowed).
+- **Position is explicit** — jumping the phasor jumps the pattern; reversing or
+ scrubbing the phase does the obvious thing. A pulse clock would need a separate
+ reset/position protocol.
+
+**No `tap.808.clock~` object in v1.** `phasor~` (+ the transport) already is the master
+clock; the 808 plan left the master as "possibly just documented patterns around
+`phasor~`/transport" — that's the recommendation. Every help patcher documents the
+idiom (`phasor~ 1.8125` ≈ 116 BPM bars, or transport-locked); if real patches later
+want A/B-fill chaining logic in an object, it can be added without touching the rows.
+
+### 3.2 The two objects
+
+**`tap.808.seq~`** — one trigger row (`sample_operator<1, 1>` + a step-index event
+outlet, §3.4):
+
+- Inlet: phase signal. Outlet 1: trigger impulses at (swung) step starts — a
+ single-sample edge whose amplitude is the step's accent value 0..1 (the family
+ contract; every `tap.808.*` voice fires on it). An optional `pulse` attribute (ms,
+ default 0 = single-sample) widens the impulse into a held gate for driving
+ `tap.adsr~`-style consumers.
+- Per-step data is a **velocity 0..1** (0 = rest) — a superset of the hardware's
+ on/accent pair. Convenience messages set the hardware view: `hits 1 0 0 1 ...`
+ (on/off list) and `accents 0 0 1 0 ...` map onto velocities `plain`/`accented`,
+ two attributes defaulting to the hardware's un-accented/accented trigger-bus levels
+ (exact values pinned at slice 0 from the 4–14 V bus mapping already in the voices).
+- The hardware's accent row falls out: rows own their accents; a patch that wants the
+ shared-bus feel sets the same `accents` list on every row.
+
+**`tap.303.seq~`** — one bass line (phase in; pitch + gate signal outlets + step-index
+event outlet):
+
+- Per-step data: `pitch` (MIDI note number, float — fractional = cents, subsuming the
+ hardware's octave flags), `gate` on/rest, `accent` flag, `slide` flag.
+- Plain step: gate rises to 1.0 at the (swung) step start, falls at `gatetime` (a
+ fraction of the step; stock value pinned at slice 0 from the service notes).
+ Accented step: gate rises to 2.0 (the voice reads accent depth = amplitude − 1; the
+ *amount* stays on the voice's accent knob, exactly like the hardware). Rest: gate
+ stays low.
+- **Slide step: the gate does not fall before it** — it holds through the boundary
+ while the pitch outlet steps to the new note; the voice's legato detection does the
+ glide and skips the retrigger. Chained slides chain; a slide from the last step into
+ the first works across the phase wrap. This is the hardware behavior verbatim
+ (§2.2.6 of the 303 plan) and costs the engine one flag.
+- `note`-shaped setters mirror the voice's message: `step 3 45 1 0` (index, pitch,
+ accent, slide), plus whole-row lists (`pitches`, `gates`, `accents`, `slides`) and a
+ live `transpose ` message.
+
+### 3.3 Shared engine features (both objects, one implementation)
+
+- **`length` 1..64** (default 16), changeable while running (takes effect immediately;
+ the step derivation is stateless in phase, so no glitch states exist).
+- **`swing` 0..1** (default 0): off-numbered steps delayed by up to a half step
+ (0 = straight, ~0.67 ≈ classic triplet shuffle), implemented as boundary warping in
+ the step derivation. Beyond-hardware (neither machine had it), so it defaults off —
+ the "documented bends, stock defaults" posture.
+- **Pattern slots**: 16 slots with `store ` / `recall `, mirroring the voices'
+ preset idiom. **`recall` is quantized** by a `quantize cycle|step|now` attribute
+ (default `cycle`): the recalled pattern arms and swaps at the next cycle (or step)
+ boundary — which *is* the 808's A/B-half and basic/fill switching, patchable from
+ one message box. Pattern storage/restore via Max `dictionary` (`dict` in/out
+ messages) for saving with the patcher, per both plans' §6.
+- **Run semantics**: the row is stateless against phase — no run/stop of its own
+ (stop the phasor, or `mute` the row). Forward-only in the data model; play the
+ phasor backwards and the row follows (documented, tested, free).
+- House invariants: allocation-free after `prepare()`, setters safe while audio runs,
+ bit-exact determinism (no randomness in this object — determinism is trivial but
+ still pinned by test), denormal-free (trivially — the engine emits constants).
+
+### 3.4 Kernel surface (in `tap/taptools`)
+
+| Header | Contents |
+|--------|----------|
+| `step_seq.h` | `taptools::seq`: the phase→step engine (boundary detection, swing warp, length, wrap) over a fixed-capacity `pattern` store (64 steps × the union payload: velocity, pitch, gate, accent, slide) with slots and quantized recall arming; two emitters composing it — `trigger_row` (impulse/pulse output, the 808 contract) and `note_row` (pitch + gate output with gate-hold slide, the 303 contract) |
+
+C++17-clean, stdlib-only, header-only, no allocation after `prepare()` — the house
+shape. The emitters are what the wrappers instantiate; the engine itself is reusable
+for any future row flavor (a CV row, a probability row) without new clock code.
+
+### 3.5 Wrapper surface (here)
+
+`source/projects/tap.808.seq_tilde/` and `source/projects/tap.303.seq_tilde/`,
+boilerplate `CMakeLists.txt` with the `CXX_STANDARD 20` override, full vertical slices
+(`docs/*.maxref.xml`, `help/*.maxhelp`, `runtime-tests/patchers/*.maxtest.maxpat`).
+Each wrapper adds a **rightmost non-signal outlet emitting the current step index**
+(deferred to the scheduler) for UI feedback (`live.grid`, step displays) — Min glue
+only, not engine state. `RELATED` cross-links each family and the other row. The
+family overview patchers get upgraded: `help/tap.808.maxhelp`'s metro/counter/sel
+pattern section is rebuilt on `phasor~` + eight `tap.808.seq~` rows (the accent-bus
+comment becomes a live demonstration), and `help/tap.303~-pattern.maxpat` is
+superseded by a `phasor~` + `tap.303.seq~` patcher (kept: the coll-based patch stays
+as the event-domain alternative it demonstrates).
+
+## 4. Work breakdown, in slices
+
+Each slice independently shippable, ending with the full definition of done (§5.4).
+
+- ✅ **Slice 0 — decisions + constants** (2026-07-18). §8 sign-off. Pin from the service notes (both
+ already in hand from the voice phases): the 808 trigger-bus plain/accented levels as
+ emitted 0..1 amplitudes (consistent with the voices' 4–14 V mapping), and the 303's
+ gate duty fraction (`gatetime` stock value). Pin the swing convention and the
+ quantized-recall semantics in the header comment.
+- ✅ **Slice 1 — the engine + the drum row, end to end** (2026-07-18). `step_seq.h` (engine +
+ `trigger_row`) + kernel tests (no bench entry — the engine is a handful of arithmetic
+ ops per sample, below the bench harness's noise floor), then `tap.808.seq~` with the full
+ vertical slice, and the `tap.808.maxhelp` overview rebuilt on real rows. De-risks
+ the clock/step derivation (the only genuinely new machinery) on the simpler emitter.
+- ✅ **Slice 2 — the bass line** (2026-07-18). `note_row` (gate-hold slide, accent amplitudes,
+ gatetime) + kernel tests, then `tap.303.seq~` with the full vertical slice, the
+ successor pattern patcher, and a cross-family demo patch (one phasor, a 303 line +
+ 808 rows; the 303 row's gate doubling the kick). The slide-across-the-wrap and
+ chained-slide cases get explicit tests.
+- ✅ **Slice 3 — patterns as data** (2026-07-18). Slots + quantized recall + `dictionary` save/recall
+ on both objects (one implementation, two surfaces), the A/B-fill idiom documented in
+ both help patchers, REVIVAL.md progress-log entries, and the two family plans' §6
+ sections marked resolved (pointing here).
+
+## 5. Testing & validation
+
+### 5.1 Kernel tests (Catch2, kernel repo CI)
+
+- **Step derivation**: boundary samples land exactly where analytic
+ `floor(phase·length)` says, across lengths 1..64, at multiple sample rates and
+ phasor frequencies; wrap-around correctness; length changes mid-cycle; reversed
+ phase.
+- **Swing**: off-step boundaries delayed by the exact warp; swing 0 is bit-identical
+ to no swing path.
+- **Trigger row**: impulse amplitude = per-step velocity; rests emit nothing; `pulse`
+ widths; a full-velocity step following a rest produces a clean below-threshold gap
+ (the voices' edge detectors re-arm).
+- **Note row**: gate rises/falls at the pinned duty; accent steps emit 2.0; **slide
+ holds the gate across the boundary with no falling edge** (assert against a
+ `tb303_voice.h` instance: the slid note does not retrigger and glides — engine and
+ voice tested as the pair they are); chained slides; slide across the wrap.
+- **Slots/recall**: quantized swap happens on the boundary sample, `now` is
+ immediate, armed recalls survive length/swing changes.
+- **House invariants**: determinism, allocation-free processing, setters during audio.
+
+### 5.2 Wrapper + runtime tests
+
+min-api unittests for attribute defaults/clamping and list/`step` message plumbing
+(mock kernel, C++17 constraint). `*.maxtest.maxpat` patchers asserting real behavior in
+Max: phasor in → triggers out at the right count per cycle, accent amplitudes, the 303
+row driving `tap.303~` (slid step audibly glides — no retrigger), step-index outlet
+firing. Local on-Mac gate per `runtime-tests/README.md`.
+
+### 5.3 Calibration
+
+Nothing to calibrate against a unit — the sequencer is logic, not circuitry; the two
+pinned constants (trigger levels, gate duty) carry service-notes provenance comments
+and are asserted by test. The musical validation is the upgraded family patchers
+playing the same patterns they played before, now sample-accurately.
+
+### 5.4 Per-slice definition of done
+
+Kernel header + tests green in kernel CI → submodule pin bumped here → wrapper builds
+universal on both platforms (CI `lipo` gate) → maxref + maxhelp + maxtest patcher →
+REVIVAL.md progress log updated.
+
+## 6. Pre-implementation questions (author decisions)
+
+### Author decisions — ✅ the three blocking ones answered (author, 2026-07-18)
+
+1. **The shape:** ✅ **approved** — one kernel engine (`step_seq.h`) + two family
+ wrappers (`tap.808.seq~` = one trigger row, `tap.303.seq~` = one pitch/gate line),
+ per §0 — i.e. 808-plan option (b) unified with the 303 emitter. (This also
+ resolves `plans/tap.808.md` §8 Q5.)
+2. **The clocking idiom:** ✅ **approved** — phase-driven (one 0..1 ramp per pattern
+ cycle from `phasor~`), **no clock object in v1**; the master clock is a documented
+ `phasor~`/transport idiom.
+3. **Pattern slots + quantized recall:** ✅ **approved** — 16 slots, `quantize`
+ defaulting to `cycle`, as the A/B-half / fill / chaining mechanism, with
+ `dictionary` save/recall.
+
+### Still open (non-blocking, accepted in principle — confirmed at their slices)
+
+4. **Per-step velocity vs. hardware flags** on `tap.808.seq~`: stored velocity 0..1
+ with `hits`/`accents` list conveniences mapping onto `plain`/`accented` levels
+ (slice-1 confirmation).
+5. **A generic third wrapper (`tap.seq~`)** exposing the engine for non-family uses —
+ deferred until someone wants it; the kernel design keeps it a thin add.
+6. **`pulse` gate-width attribute** on the trigger row for envelope consumers — in
+ (cheap, contained); confirm at slice 1.
+7. **Step-index UI outlet** — in (scheduler-domain, Min-glue only); confirm at
+ slice 1.
+
+## 7. Sources
+
+- `plans/tap.808.md` §6 and `plans/tap.303.md` §6 — the deferred phase-3 designs this
+ plan resolves; the voice contracts in those plans' §3.2 (both author-approved
+ 2026-07-17) are the emitted interfaces.
+- Roland TR-808 owner's manual / service notes — sequencer section: steps, A/B halves,
+ fills, pre-scale, the accent row and AC bus (trigger levels pinned at slice 0).
+- Roland TB-303 owner's manual / service notes — pattern memory data model (note,
+ gate, accent, slide, octave), gate timing (duty pinned at slice 0); Robin Whittle's
+ Devil Fish notes on gate/slide behavior (already in hand from the voice phases).
+- `help/tap.303~-pattern.maxpat` — the phase-3 interface dry run (event-domain);
+ `help/tap.808.maxhelp` — the family pattern section both slices upgrade.
diff --git a/runtime-tests/patchers/tap.303.seq~.maxtest.maxpat b/runtime-tests/patchers/tap.303.seq~.maxtest.maxpat
new file mode 100644
index 00000000..56ec382b
--- /dev/null
+++ b/runtime-tests/patchers/tap.303.seq~.maxtest.maxpat
@@ -0,0 +1,356 @@
+{
+ "patcher": {
+ "fileversion": 1,
+ "appversion": {
+ "major": 8,
+ "minor": 6,
+ "revision": 2,
+ "architecture": "x64",
+ "modernui": 1
+ },
+ "classnamespace": "box",
+ "rect": [
+ 66.0,
+ 79.0,
+ 1070.0,
+ 480.0
+ ],
+ "bglocked": 0,
+ "openinpresentation": 0,
+ "default_fontsize": 11.0,
+ "default_fontface": 0,
+ "default_fontname": "Arial",
+ "gridonopen": 1,
+ "gridsize": [
+ 15.0,
+ 15.0
+ ],
+ "gridsnaponopen": 1,
+ "statusbarvisible": 2,
+ "toolbarvisible": 1,
+ "description": "tap.303.seq~ loads and is silent with an empty pattern (open in Max to extend: phasor~ in -> triggers out, accent amplitudes, slide legato).",
+ "boxes": [
+ {
+ "box": {
+ "id": "obj-1",
+ "maxclass": "newobj",
+ "numinlets": 1,
+ "numoutlets": 1,
+ "outlettype": [
+ "bang"
+ ],
+ "patching_rect": [
+ 40.0,
+ 40.0,
+ 70.0,
+ 22.0
+ ],
+ "text": "loadbang"
+ }
+ },
+ {
+ "box": {
+ "id": "obj-2",
+ "maxclass": "button",
+ "numinlets": 1,
+ "numoutlets": 1,
+ "outlettype": [
+ "bang"
+ ],
+ "patching_rect": [
+ 40.0,
+ 90.0,
+ 24.0,
+ 24.0
+ ]
+ }
+ },
+ {
+ "box": {
+ "id": "obj-3",
+ "maxclass": "message",
+ "numinlets": 2,
+ "numoutlets": 1,
+ "outlettype": [
+ ""
+ ],
+ "patching_rect": [
+ 40.0,
+ 140.0,
+ 30.0,
+ 20.0
+ ],
+ "text": "1"
+ }
+ },
+ {
+ "box": {
+ "id": "obj-4",
+ "maxclass": "newobj",
+ "numinlets": 2,
+ "numoutlets": 0,
+ "outlettype": [],
+ "patching_rect": [
+ 40.0,
+ 190.0,
+ 45.0,
+ 22.0
+ ],
+ "text": "dac~"
+ }
+ },
+ {
+ "box": {
+ "id": "obj-5",
+ "maxclass": "newobj",
+ "numinlets": 1,
+ "numoutlets": 1,
+ "outlettype": [
+ ""
+ ],
+ "patching_rect": [
+ 120.0,
+ 140.0,
+ 110.0,
+ 22.0
+ ],
+ "text": "test.terminate"
+ }
+ },
+ {
+ "box": {
+ "id": "obj-6",
+ "maxclass": "newobj",
+ "numinlets": 1,
+ "numoutlets": 1,
+ "outlettype": [
+ "signal"
+ ],
+ "patching_rect": [
+ 300.0,
+ 40.0,
+ 110.0,
+ 22.0
+ ],
+ "text": "sig~ 0.0"
+ }
+ },
+ {
+ "box": {
+ "id": "obj-7",
+ "maxclass": "newobj",
+ "numinlets": 1,
+ "numoutlets": 3,
+ "outlettype": [
+ "signal",
+ "signal",
+ ""
+ ],
+ "patching_rect": [
+ 300.0,
+ 100.0,
+ 240.0,
+ 22.0
+ ],
+ "text": "tap.303.seq~"
+ }
+ },
+ {
+ "box": {
+ "id": "obj-8",
+ "maxclass": "newobj",
+ "numinlets": 1,
+ "numoutlets": 1,
+ "outlettype": [
+ "signal"
+ ],
+ "patching_rect": [
+ 300.0,
+ 160.0,
+ 110.0,
+ 22.0
+ ],
+ "text": "round~ 1e-06"
+ }
+ },
+ {
+ "box": {
+ "id": "obj-9",
+ "maxclass": "newobj",
+ "numinlets": 1,
+ "numoutlets": 1,
+ "outlettype": [
+ ""
+ ],
+ "patching_rect": [
+ 300.0,
+ 220.0,
+ 90.0,
+ 22.0
+ ],
+ "text": "test.sample~"
+ }
+ },
+ {
+ "box": {
+ "id": "obj-10",
+ "maxclass": "newobj",
+ "numinlets": 1,
+ "numoutlets": 1,
+ "outlettype": [
+ ""
+ ],
+ "patching_rect": [
+ 300.0,
+ 280.0,
+ 150.0,
+ 22.0
+ ],
+ "text": "test.equals 0.0"
+ }
+ },
+ {
+ "box": {
+ "id": "obj-11",
+ "maxclass": "newobj",
+ "numinlets": 1,
+ "numoutlets": 1,
+ "outlettype": [
+ ""
+ ],
+ "patching_rect": [
+ 300.0,
+ 340.0,
+ 260.0,
+ 22.0
+ ],
+ "text": "test.assert 303-seq-silent-at-rest"
+ }
+ }
+ ],
+ "lines": [
+ {
+ "patchline": {
+ "source": [
+ "obj-1",
+ 0
+ ],
+ "destination": [
+ "obj-2",
+ 0
+ ]
+ }
+ },
+ {
+ "patchline": {
+ "source": [
+ "obj-2",
+ 0
+ ],
+ "destination": [
+ "obj-3",
+ 0
+ ]
+ }
+ },
+ {
+ "patchline": {
+ "source": [
+ "obj-3",
+ 0
+ ],
+ "destination": [
+ "obj-4",
+ 0
+ ]
+ }
+ },
+ {
+ "patchline": {
+ "source": [
+ "obj-2",
+ 0
+ ],
+ "destination": [
+ "obj-5",
+ 0
+ ]
+ }
+ },
+ {
+ "patchline": {
+ "source": [
+ "obj-6",
+ 0
+ ],
+ "destination": [
+ "obj-7",
+ 0
+ ]
+ }
+ },
+ {
+ "patchline": {
+ "source": [
+ "obj-7",
+ 1
+ ],
+ "destination": [
+ "obj-8",
+ 0
+ ]
+ }
+ },
+ {
+ "patchline": {
+ "source": [
+ "obj-8",
+ 0
+ ],
+ "destination": [
+ "obj-9",
+ 0
+ ]
+ }
+ },
+ {
+ "patchline": {
+ "source": [
+ "obj-9",
+ 0
+ ],
+ "destination": [
+ "obj-2",
+ 0
+ ]
+ }
+ },
+ {
+ "patchline": {
+ "source": [
+ "obj-9",
+ 0
+ ],
+ "destination": [
+ "obj-10",
+ 0
+ ]
+ }
+ },
+ {
+ "patchline": {
+ "source": [
+ "obj-10",
+ 0
+ ],
+ "destination": [
+ "obj-11",
+ 0
+ ]
+ }
+ }
+ ],
+ "dependency_cache": [],
+ "autosave": 0
+ }
+}
diff --git a/runtime-tests/patchers/tap.808.seq~.maxtest.maxpat b/runtime-tests/patchers/tap.808.seq~.maxtest.maxpat
new file mode 100644
index 00000000..558d4d83
--- /dev/null
+++ b/runtime-tests/patchers/tap.808.seq~.maxtest.maxpat
@@ -0,0 +1,355 @@
+{
+ "patcher": {
+ "fileversion": 1,
+ "appversion": {
+ "major": 8,
+ "minor": 6,
+ "revision": 2,
+ "architecture": "x64",
+ "modernui": 1
+ },
+ "classnamespace": "box",
+ "rect": [
+ 66.0,
+ 79.0,
+ 1070.0,
+ 480.0
+ ],
+ "bglocked": 0,
+ "openinpresentation": 0,
+ "default_fontsize": 11.0,
+ "default_fontface": 0,
+ "default_fontname": "Arial",
+ "gridonopen": 1,
+ "gridsize": [
+ 15.0,
+ 15.0
+ ],
+ "gridsnaponopen": 1,
+ "statusbarvisible": 2,
+ "toolbarvisible": 1,
+ "description": "tap.808.seq~ loads and is silent with an empty pattern (open in Max to extend: phasor~ in -> triggers out, accent amplitudes, slide legato).",
+ "boxes": [
+ {
+ "box": {
+ "id": "obj-1",
+ "maxclass": "newobj",
+ "numinlets": 1,
+ "numoutlets": 1,
+ "outlettype": [
+ "bang"
+ ],
+ "patching_rect": [
+ 40.0,
+ 40.0,
+ 70.0,
+ 22.0
+ ],
+ "text": "loadbang"
+ }
+ },
+ {
+ "box": {
+ "id": "obj-2",
+ "maxclass": "button",
+ "numinlets": 1,
+ "numoutlets": 1,
+ "outlettype": [
+ "bang"
+ ],
+ "patching_rect": [
+ 40.0,
+ 90.0,
+ 24.0,
+ 24.0
+ ]
+ }
+ },
+ {
+ "box": {
+ "id": "obj-3",
+ "maxclass": "message",
+ "numinlets": 2,
+ "numoutlets": 1,
+ "outlettype": [
+ ""
+ ],
+ "patching_rect": [
+ 40.0,
+ 140.0,
+ 30.0,
+ 20.0
+ ],
+ "text": "1"
+ }
+ },
+ {
+ "box": {
+ "id": "obj-4",
+ "maxclass": "newobj",
+ "numinlets": 2,
+ "numoutlets": 0,
+ "outlettype": [],
+ "patching_rect": [
+ 40.0,
+ 190.0,
+ 45.0,
+ 22.0
+ ],
+ "text": "dac~"
+ }
+ },
+ {
+ "box": {
+ "id": "obj-5",
+ "maxclass": "newobj",
+ "numinlets": 1,
+ "numoutlets": 1,
+ "outlettype": [
+ ""
+ ],
+ "patching_rect": [
+ 120.0,
+ 140.0,
+ 110.0,
+ 22.0
+ ],
+ "text": "test.terminate"
+ }
+ },
+ {
+ "box": {
+ "id": "obj-6",
+ "maxclass": "newobj",
+ "numinlets": 1,
+ "numoutlets": 1,
+ "outlettype": [
+ "signal"
+ ],
+ "patching_rect": [
+ 300.0,
+ 40.0,
+ 110.0,
+ 22.0
+ ],
+ "text": "sig~ 0.0"
+ }
+ },
+ {
+ "box": {
+ "id": "obj-7",
+ "maxclass": "newobj",
+ "numinlets": 1,
+ "numoutlets": 2,
+ "outlettype": [
+ "signal",
+ ""
+ ],
+ "patching_rect": [
+ 300.0,
+ 100.0,
+ 240.0,
+ 22.0
+ ],
+ "text": "tap.808.seq~"
+ }
+ },
+ {
+ "box": {
+ "id": "obj-8",
+ "maxclass": "newobj",
+ "numinlets": 1,
+ "numoutlets": 1,
+ "outlettype": [
+ "signal"
+ ],
+ "patching_rect": [
+ 300.0,
+ 160.0,
+ 110.0,
+ 22.0
+ ],
+ "text": "round~ 1e-06"
+ }
+ },
+ {
+ "box": {
+ "id": "obj-9",
+ "maxclass": "newobj",
+ "numinlets": 1,
+ "numoutlets": 1,
+ "outlettype": [
+ ""
+ ],
+ "patching_rect": [
+ 300.0,
+ 220.0,
+ 90.0,
+ 22.0
+ ],
+ "text": "test.sample~"
+ }
+ },
+ {
+ "box": {
+ "id": "obj-10",
+ "maxclass": "newobj",
+ "numinlets": 1,
+ "numoutlets": 1,
+ "outlettype": [
+ ""
+ ],
+ "patching_rect": [
+ 300.0,
+ 280.0,
+ 150.0,
+ 22.0
+ ],
+ "text": "test.equals 0.0"
+ }
+ },
+ {
+ "box": {
+ "id": "obj-11",
+ "maxclass": "newobj",
+ "numinlets": 1,
+ "numoutlets": 1,
+ "outlettype": [
+ ""
+ ],
+ "patching_rect": [
+ 300.0,
+ 340.0,
+ 260.0,
+ 22.0
+ ],
+ "text": "test.assert 808-seq-silent-at-rest"
+ }
+ }
+ ],
+ "lines": [
+ {
+ "patchline": {
+ "source": [
+ "obj-1",
+ 0
+ ],
+ "destination": [
+ "obj-2",
+ 0
+ ]
+ }
+ },
+ {
+ "patchline": {
+ "source": [
+ "obj-2",
+ 0
+ ],
+ "destination": [
+ "obj-3",
+ 0
+ ]
+ }
+ },
+ {
+ "patchline": {
+ "source": [
+ "obj-3",
+ 0
+ ],
+ "destination": [
+ "obj-4",
+ 0
+ ]
+ }
+ },
+ {
+ "patchline": {
+ "source": [
+ "obj-2",
+ 0
+ ],
+ "destination": [
+ "obj-5",
+ 0
+ ]
+ }
+ },
+ {
+ "patchline": {
+ "source": [
+ "obj-6",
+ 0
+ ],
+ "destination": [
+ "obj-7",
+ 0
+ ]
+ }
+ },
+ {
+ "patchline": {
+ "source": [
+ "obj-7",
+ 0
+ ],
+ "destination": [
+ "obj-8",
+ 0
+ ]
+ }
+ },
+ {
+ "patchline": {
+ "source": [
+ "obj-8",
+ 0
+ ],
+ "destination": [
+ "obj-9",
+ 0
+ ]
+ }
+ },
+ {
+ "patchline": {
+ "source": [
+ "obj-9",
+ 0
+ ],
+ "destination": [
+ "obj-2",
+ 0
+ ]
+ }
+ },
+ {
+ "patchline": {
+ "source": [
+ "obj-9",
+ 0
+ ],
+ "destination": [
+ "obj-10",
+ 0
+ ]
+ }
+ },
+ {
+ "patchline": {
+ "source": [
+ "obj-10",
+ 0
+ ],
+ "destination": [
+ "obj-11",
+ 0
+ ]
+ }
+ }
+ ],
+ "dependency_cache": [],
+ "autosave": 0
+ }
+}
diff --git a/source/projects/tap.303.seq_tilde/CMakeLists.txt b/source/projects/tap.303.seq_tilde/CMakeLists.txt
new file mode 100644
index 00000000..be5db753
--- /dev/null
+++ b/source/projects/tap.303.seq_tilde/CMakeLists.txt
@@ -0,0 +1,36 @@
+# Copyright 2026 Timothy Place. Distributed under the New BSD License.
+
+cmake_minimum_required(VERSION 3.19)
+
+set(C74_MIN_API_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../min-api)
+include(${C74_MIN_API_DIR}/script/min-pretarget.cmake)
+
+#############################################################
+# MAX EXTERNAL
+#############################################################
+
+include_directories("${C74_INCLUDES}")
+# The portable DSP kernel (header-only), pinned as the submodules/taptools submodule.
+include_directories("${TAPTOOLS_KERNEL_DIR}/include")
+
+set(SOURCES
+ ${PROJECT_NAME}.cpp
+ ${TAPTOOLS_KERNEL_DIR}/include/taptools/step_seq.h
+)
+
+add_library(
+ ${PROJECT_NAME}
+ MODULE
+ ${SOURCES}
+)
+
+include(${C74_MIN_API_DIR}/script/min-posttarget.cmake)
+
+# TapTools targets C++20. Min's posttarget pins the standard to C++17, so override it here.
+set_target_properties(${PROJECT_NAME} PROPERTIES
+ CXX_STANDARD 20
+ CXX_STANDARD_REQUIRED ON
+)
+
+# Catch-based unit tests (built when the _test.cpp is present; run via ctest).
+include(${C74_MIN_API_DIR}/test/min-object-unittest.cmake)
diff --git a/source/projects/tap.303.seq_tilde/tap.303.seq_tilde.cpp b/source/projects/tap.303.seq_tilde/tap.303.seq_tilde.cpp
new file mode 100644
index 00000000..bbd6c65c
--- /dev/null
+++ b/source/projects/tap.303.seq_tilde/tap.303.seq_tilde.cpp
@@ -0,0 +1,327 @@
+/// @file
+/// @brief tap.303.seq~ — one step-sequenced bass line (the tap.303~ note contract).
+/// @details A thin Min wrapper around the shared step-sequencer kernel in step_seq.h
+/// (taptools::seq::note_row — the engine, the swing warp, the slot store, the
+/// pinned gate duty and accent levels all live there; see its header for the
+/// model and plans/tap.seq.md for the design of record).
+///
+/// The clocking idiom: the inlet takes a phase ramp, 0..1 per pattern cycle —
+/// connect phasor~ (transport-locked if you like); tap.808.seq~ rows on the same
+/// phasor stay sample-locked with it. The row emits exactly the tap.303~ inlet
+/// pair: a pitch signal (MIDI note number, held between notes) and a gate signal
+/// whose edge amplitude is 1.0 for a plain step, 2.0 for an accented one (the
+/// voice reads accent depth = amplitude - 1; the accent *amount* stays on the
+/// voice's knob, like the hardware). The gate opens at the step start and closes
+/// halfway through the step (Open303's measured duty). A step with the slide
+/// flag is approached legato: the gate holds through the boundary while the
+/// pitch steps, and the voice's ~60 ms glide does the rest — no retrigger,
+/// chained slides chain, and a slide across the pattern wrap works.
+///
+/// Per-step data mirrors the hardware pattern memory: pitch, gate/rest, accent,
+/// slide (pitch as a MIDI float subsumes the octave up/down flags; transpose
+/// live with @transpose). The 1981 entry workflow is not recreated — patterns
+/// are lists, step messages, slots, and dictionaries.
+/// @author Timothy Place
+/// @copyright Copyright 2026 Timothy Place. Distributed under the New BSD License.
+
+#include
+
+#include
+
+#include "c74_min.h"
+
+using namespace c74::min;
+namespace kernel = taptools::seq;
+
+class seq303 : public object, public vector_operator<> {
+ private:
+ // Constructed before the attributes below so attribute setters can forward into it.
+ kernel::note_row m_row;
+ std::atomic m_step_now{-1};
+ int m_step_sent{-1};
+
+ public:
+ MIN_DESCRIPTION{"A step-sequenced acid bass line for tap.303~. Feed a phase ramp (phasor~, "
+ "0..1 per pattern cycle) and the row emits the voice's inlet pair: pitch "
+ "(MIDI note number) and a gate whose edge amplitude encodes accent (1 "
+ "plain, 2 accented), with the hardware's slide realized as gate-hold across "
+ "the step boundary — the voice glides legato without retriggering. Per-step "
+ "pitch, gate/rest, accent, and slide; sixteen steps by default (up to 64), "
+ "swing, live transpose, 16 pattern slots with boundary-quantized recall, "
+ "and dictionary save/recall. The gate edge also triggers any tap.808.* "
+ "voice — a bass line doubling a kick is one patch cord."};
+ MIN_TAGS{"sequencing, synthesis"};
+ MIN_AUTHOR{"Timothy Place"};
+ MIN_RELATED{"tap.303~, tap.808.seq~, tap.adsr~, phasor~"};
+
+ inlet<> m_in{this, "(signal) phase ramp: 0..1 per pattern cycle (connect phasor~)"};
+ outlet<> m_out_pitch{this, "(signal) pitch as MIDI note number -> tap.303~ left inlet", "signal"};
+ outlet<> m_out_gate{this, "(signal) gate; edge amplitude 1..2 = accent -> tap.303~ right inlet", "signal"};
+ outlet<> m_out_step{this, "(int) current step number, 1-based, at each step change"};
+
+ seq303(const atoms& args = {}) { m_row.prepare(samplerate()); }
+
+ attribute length{this, "length", kernel::k_default_length, setter{ MIN_FUNCTION {
+ const int v = std::clamp(static_cast(args[0]), 1, kernel::k_max_steps);
+ m_row.clock().data().set_length(v);
+ return {v};
+ }},
+ description{"Steps per pattern cycle (1..64, default 16). A different length than the "
+ "other rows on the same phasor is polymeter."}};
+
+ attribute swing{this, "swing", 0.0, setter{ MIN_FUNCTION {
+ const double v = std::clamp(static_cast(args[0]), 0.0, 1.0);
+ m_row.clock().set_swing(v);
+ return {v};
+ }},
+ description{"Swing 0..1: odd-numbered steps play late by up to half a step (0.67 is "
+ "the classic triplet shuffle). The hardware had none — 0 is stock."}};
+
+ attribute transpose{this, "transpose", 0.0, setter{ MIN_FUNCTION {
+ const double v = std::clamp(static_cast(args[0]), -48.0, 48.0);
+ m_row.set_transpose(v);
+ return {v};
+ }},
+ description{"Live transposition in semitones (-48..48), applied as notes fire — "
+ "what the hardware's transpose mode did, without the mode."}};
+
+ attribute quantize{this, "quantize", "cycle", setter{ MIN_FUNCTION {
+ const symbol s = args[0];
+ const int m = (s == symbol("now")) ? kernel::quantize_now
+ : (s == symbol("step")) ? kernel::quantize_step
+ : kernel::quantize_cycle;
+ m_row.clock().set_quantize(m);
+ return {m == kernel::quantize_now ? symbol("now")
+ : m == kernel::quantize_step ? symbol("step")
+ : symbol("cycle")};
+ }},
+ description{"When a recalled pattern takes over: cycle (default — the next "
+ "pattern top), step, or now."}};
+
+ attribute mute{this, "mute", false,
+ description{"Force the gate low (the clock keeps running; pitch holds)."}};
+
+ // -- messages -------------------------------------------------------------------------------------
+
+ message<> step{this, "step",
+ "Set one step and gate it on: step [accent 0/1] [slide 0/1]. "
+ "Pitch is a MIDI note number; fractional values are cents.",
+ MIN_FUNCTION {
+ if (args.size() < 2) {
+ return {};
+ }
+ const int k = static_cast(args[0]) - 1;
+ if (k < 0 || k >= kernel::k_max_steps) {
+ return {};
+ }
+ auto& st = m_row.clock().data().steps[k];
+ st.pitch = args[1];
+ st.gate = true;
+ st.accent = args.size() > 2 && static_cast(args[2]) != 0;
+ st.slide = args.size() > 3 && static_cast(args[3]) != 0;
+ return {};
+ }};
+
+ message<> rest{this, "rest", "Silence one step: rest .",
+ MIN_FUNCTION {
+ if (args.empty()) {
+ return {};
+ }
+ const int k = static_cast(args[0]) - 1;
+ if (k >= 0 && k < kernel::k_max_steps) {
+ m_row.clock().data().steps[k].gate = false;
+ }
+ return {};
+ }};
+
+ message<> pitches{this, "pitches", "Set per-step pitches (MIDI note numbers): pitches ... .",
+ MIN_FUNCTION {
+ auto& p = m_row.clock().data();
+ const size_t n = std::min(args.size(), static_cast(kernel::k_max_steps));
+ for (size_t k = 0; k < n; ++k) {
+ p.steps[k].pitch = args[k];
+ }
+ return {};
+ }};
+
+ message<> gates{this, "gates", "Set which steps sound: gates <0/1> ... (0 = rest).",
+ MIN_FUNCTION {
+ auto& p = m_row.clock().data();
+ const size_t n = std::min(args.size(), static_cast(kernel::k_max_steps));
+ for (size_t k = 0; k < n; ++k) {
+ p.steps[k].gate = static_cast(args[k]) != 0;
+ }
+ return {};
+ }};
+
+ message<> accents{this, "accents", "Set per-step accent flags: accents <0/1> ....",
+ MIN_FUNCTION {
+ auto& p = m_row.clock().data();
+ const size_t n = std::min(args.size(), static_cast(kernel::k_max_steps));
+ for (size_t k = 0; k < n; ++k) {
+ p.steps[k].accent = static_cast(args[k]) != 0;
+ }
+ return {};
+ }};
+
+ message<> slides{this, "slides",
+ "Set per-step slide flags: slides <0/1> .... A flagged step is approached legato — "
+ "the gate holds through the boundary and the voice glides to it.",
+ MIN_FUNCTION {
+ auto& p = m_row.clock().data();
+ const size_t n = std::min(args.size(), static_cast(kernel::k_max_steps));
+ for (size_t k = 0; k < n; ++k) {
+ p.steps[k].slide = static_cast(args[k]) != 0;
+ }
+ return {};
+ }};
+
+ message<> store{this, "store", "Store the pattern in a slot (1..16).",
+ MIN_FUNCTION {
+ if (!args.empty()) {
+ m_row.clock().store(static_cast(args[0]) - 1);
+ }
+ return {};
+ }};
+
+ message<> recall{this, "recall",
+ "Recall a slot (1..16); takes over at the quantize boundary (default: the next "
+ "pattern top).",
+ MIN_FUNCTION {
+ if (!args.empty()) {
+ m_row.clock().recall(static_cast(args[0]) - 1);
+ }
+ return {};
+ }};
+
+ message<> todict{this, "todict", "Write the pattern into a named dictionary: todict .",
+ MIN_FUNCTION {
+ if (args.empty()) {
+ return {};
+ }
+ dict d{symbol(args[0])};
+ auto& p = m_row.clock().data();
+ atoms pitch_list, gate_list, accent_list, slide_list;
+ for (int k = 0; k < p.length; ++k) {
+ pitch_list.push_back(p.steps[k].pitch);
+ gate_list.push_back(p.steps[k].gate ? 1 : 0);
+ accent_list.push_back(p.steps[k].accent ? 1 : 0);
+ slide_list.push_back(p.steps[k].slide ? 1 : 0);
+ }
+ set_key(d, "length", atoms{p.length});
+ set_key(d, "pitches", pitch_list);
+ set_key(d, "gates", gate_list);
+ set_key(d, "accents", accent_list);
+ set_key(d, "slides", slide_list);
+ d.touch();
+ return {};
+ }};
+
+ message<> fromdict{this, "fromdict", "Read a pattern written by todict: fromdict .",
+ MIN_FUNCTION {
+ if (args.empty()) {
+ return {};
+ }
+ try {
+ dict d{symbol(args[0])};
+ const atoms ln = d.at("length");
+ const atoms pv = d.at("pitches");
+ const atoms gv = d.at("gates");
+ const atoms av = d.at("accents");
+ const atoms sv = d.at("slides");
+ if (ln.empty()) {
+ throw std::runtime_error("empty");
+ }
+ const int n = std::clamp(static_cast(ln[0]), 1, kernel::k_max_steps);
+ auto& p = m_row.clock().data();
+ p.set_length(n);
+ length = n;
+ for (int k = 0; k < n; ++k) {
+ auto& st = p.steps[k];
+ if (k < static_cast(pv.size())) {
+ st.pitch = pv[k];
+ }
+ if (k < static_cast(gv.size())) {
+ st.gate = static_cast(gv[k]) != 0;
+ }
+ if (k < static_cast(av.size())) {
+ st.accent = static_cast(av[k]) != 0;
+ }
+ if (k < static_cast(sv.size())) {
+ st.slide = static_cast(sv[k]) != 0;
+ }
+ }
+ }
+ catch (...) {
+ cerr << "fromdict: no pattern found in that dictionary" << endl;
+ }
+ return {};
+ }};
+
+ message<> clear{this, "clear", "Empty the pattern (all rests) and reset the clock state.",
+ MIN_FUNCTION {
+ m_row.clock().data().clear();
+ m_row.reset();
+ return {};
+ }};
+
+ message<> dspsetup{this, "dspsetup", "Reconfigure for the sample rate when DSP starts.",
+ MIN_FUNCTION {
+ m_row.prepare(samplerate());
+ return {};
+ }};
+
+ // Step-change notification: coalescing single-value handoff from the audio thread to the
+ // scheduler (the tap.sift~ ring's little sibling — the newest index wins).
+ timer<> m_notify{this,
+ MIN_FUNCTION {
+ const int k = m_step_now.load(std::memory_order_acquire);
+ if (k >= 0 && k != m_step_sent) {
+ m_step_sent = k;
+ m_out_step.send(k + 1);
+ }
+ return {};
+ }};
+
+ // -- perform --------------------------------------------------------------------------------------
+
+ void operator()(audio_bundle input, audio_bundle output) {
+ const long n = input.frame_count();
+ const double* in = input.samples(0);
+ double* pitch_out = output.samples(0);
+ double* gate_out = output.samples(1);
+ const bool muted = mute;
+
+ if (!m_in.has_signal_connection()) {
+ std::fill_n(pitch_out, n, 0.0);
+ std::fill_n(gate_out, n, 0.0);
+ return;
+ }
+ for (long i = 0; i < n; ++i) {
+ const auto o = m_row.process(in[i]);
+ pitch_out[i] = o.pitch;
+ gate_out[i] = muted ? 0.0 : o.gate;
+ }
+ const int k = m_row.clock().current_step();
+ if (k != m_step_now.load(std::memory_order_relaxed)) {
+ m_step_now.store(k, std::memory_order_release);
+ m_notify.delay(0);
+ }
+ }
+
+ // Set a list value on a dictionary key (atom_reference only assigns scalars).
+ static void set_key(dict& d, const char* key, const atoms& v) {
+ if (v.empty()) {
+ return;
+ }
+ c74::max::dictionary_appendatoms(reinterpret_cast(static_cast(d)),
+ symbol(key), static_cast(v.size()),
+ reinterpret_cast(const_cast(v.data())));
+ }
+
+ // Exposed for unit tests.
+ public:
+ kernel::note_row& row() { return m_row; }
+};
+
+MIN_EXTERNAL(seq303);
diff --git a/source/projects/tap.303.seq_tilde/tap.303.seq_tilde_test.cpp b/source/projects/tap.303.seq_tilde/tap.303.seq_tilde_test.cpp
new file mode 100644
index 00000000..48238ef9
--- /dev/null
+++ b/source/projects/tap.303.seq_tilde/tap.303.seq_tilde_test.cpp
@@ -0,0 +1,88 @@
+/// @file
+/// @brief Unit tests for tap.303.seq~ (Min-level: attribute defaults, clamping, plumbing).
+// SPDX-License-Identifier: BSD-3-Clause
+// Copyright 2026 Timothy Place.
+
+#include "c74_min_unittest.h" // required unit-test header (defines main via Catch)
+#include "tap.303.seq_tilde.cpp" // include the object source so we can instantiate it
+
+SCENARIO("tap.303.seq~ instantiates with the plan's defaults") {
+ ext_main(nullptr); // configure the class (required once per test executable)
+
+ GIVEN("a default instance") {
+ test_wrapper an_instance;
+ seq303& my_object = an_instance;
+
+ THEN("the pattern is 16 steps, straight, untransposed, cycle-quantized") {
+ REQUIRE(static_cast(my_object.length) == 16);
+ REQUIRE(static_cast(my_object.swing) == 0.0);
+ REQUIRE(static_cast(my_object.transpose) == 0.0);
+ REQUIRE(my_object.quantize == symbol("cycle"));
+ }
+ THEN("all steps rest") {
+ auto& p = my_object.row().clock().data();
+ for (int k = 0; k < p.length; ++k) {
+ REQUIRE(p.steps[k].gate == false);
+ }
+ }
+ }
+}
+
+SCENARIO("tap.303.seq~ plumbs the pattern surface into the kernel") {
+ ext_main(nullptr);
+
+ GIVEN("an instance") {
+ test_wrapper an_instance;
+ seq303& my_object = an_instance;
+
+ WHEN("a step message sets pitch, accent, and slide") {
+ my_object.step(c74::min::atoms{3, 45.0, 1, 1}); // 1-based
+ THEN("step 3 is gated with the flags") {
+ auto& st = my_object.row().clock().data().steps[2];
+ REQUIRE(st.gate == true);
+ REQUIRE(st.pitch == 45.0);
+ REQUIRE(st.accent == true);
+ REQUIRE(st.slide == true);
+ }
+ }
+ WHEN("rest silences it again") {
+ my_object.step(c74::min::atoms{3, 45.0});
+ my_object.rest(c74::min::atoms{3});
+ THEN("the gate clears but the pitch stays") {
+ auto& st = my_object.row().clock().data().steps[2];
+ REQUIRE(st.gate == false);
+ REQUIRE(st.pitch == 45.0);
+ }
+ }
+ WHEN("the list surface writes a line") {
+ my_object.pitches(c74::min::atoms{33.0, 45.0, 36.0});
+ my_object.gates(c74::min::atoms{1, 1, 0});
+ my_object.accents(c74::min::atoms{1, 0, 0});
+ my_object.slides(c74::min::atoms{0, 1, 0});
+ THEN("the kernel pattern holds it") {
+ auto& p = my_object.row().clock().data();
+ REQUIRE(p.steps[0].pitch == 33.0);
+ REQUIRE(p.steps[0].accent == true);
+ REQUIRE(p.steps[1].slide == true);
+ REQUIRE(p.steps[2].gate == false);
+ }
+ }
+ WHEN("transpose is set out of range") {
+ my_object.transpose = 90.0;
+ THEN("it clamps to 48 and reaches the row") {
+ REQUIRE(static_cast(my_object.transpose) == 48.0);
+ REQUIRE(my_object.row().transpose() == 48.0);
+ }
+ }
+ WHEN("quantize is set to now and a stored slot is recalled") {
+ my_object.step(c74::min::atoms{1, 40.0});
+ my_object.store(c74::min::atoms{2});
+ my_object.rest(c74::min::atoms{1});
+ my_object.quantize = c74::min::symbol("now");
+ my_object.recall(c74::min::atoms{2});
+ THEN("the slot applies immediately") {
+ REQUIRE(my_object.row().clock().data().steps[0].gate == true);
+ }
+ }
+ }
+}
diff --git a/source/projects/tap.303_tilde/tap.303_tilde.cpp b/source/projects/tap.303_tilde/tap.303_tilde.cpp
index eeb3762e..2bd21bf1 100644
--- a/source/projects/tap.303_tilde/tap.303_tilde.cpp
+++ b/source/projects/tap.303_tilde/tap.303_tilde.cpp
@@ -48,7 +48,7 @@ class voice303 : public object, public vector_operator<> {
"in mc. for more 303s than anyone needs."};
MIN_TAGS{"synthesis"};
MIN_AUTHOR{"Timothy Place"};
- MIN_RELATED{"tap.diode~, tap.vco~, tap.808.kick~, tap.adsr~, mc.noteallocator~"};
+ MIN_RELATED{"tap.303.seq~, tap.diode~, tap.vco~, tap.808.kick~, tap.adsr~"};
inlet<> m_in_pitch{this, "(signal/float) pitch as MIDI note number"};
inlet<> m_in_gate{this, "(signal) gate; edge amplitude 1..2 = accent"};
diff --git a/source/projects/tap.808.kick_tilde/tap.808.kick_tilde.cpp b/source/projects/tap.808.kick_tilde/tap.808.kick_tilde.cpp
index 388fe6dc..954db0b2 100644
--- a/source/projects/tap.808.kick_tilde/tap.808.kick_tilde.cpp
+++ b/source/projects/tap.808.kick_tilde/tap.808.kick_tilde.cpp
@@ -43,7 +43,7 @@ class kick808 : public object, public sample_operator<1, 1> {
"defaults."};
MIN_TAGS{"generators, drums"};
MIN_AUTHOR{"Timothy Place"};
- MIN_RELATED{"tap.808.snare~, tap.808.tom~, tap.808.hat~, click~, kick~"};
+ MIN_RELATED{"tap.808.seq~, tap.808.snare~, tap.808.tom~, tap.808.hat~, click~"};
inlet<> m_in{this, "(signal) trigger: rising edge fires, edge amplitude 0..1 is the accent"};
outlet<> m_out{this, "(signal) bass drum output", "signal"};
diff --git a/source/projects/tap.808.seq_tilde/CMakeLists.txt b/source/projects/tap.808.seq_tilde/CMakeLists.txt
new file mode 100644
index 00000000..be5db753
--- /dev/null
+++ b/source/projects/tap.808.seq_tilde/CMakeLists.txt
@@ -0,0 +1,36 @@
+# Copyright 2026 Timothy Place. Distributed under the New BSD License.
+
+cmake_minimum_required(VERSION 3.19)
+
+set(C74_MIN_API_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../min-api)
+include(${C74_MIN_API_DIR}/script/min-pretarget.cmake)
+
+#############################################################
+# MAX EXTERNAL
+#############################################################
+
+include_directories("${C74_INCLUDES}")
+# The portable DSP kernel (header-only), pinned as the submodules/taptools submodule.
+include_directories("${TAPTOOLS_KERNEL_DIR}/include")
+
+set(SOURCES
+ ${PROJECT_NAME}.cpp
+ ${TAPTOOLS_KERNEL_DIR}/include/taptools/step_seq.h
+)
+
+add_library(
+ ${PROJECT_NAME}
+ MODULE
+ ${SOURCES}
+)
+
+include(${C74_MIN_API_DIR}/script/min-posttarget.cmake)
+
+# TapTools targets C++20. Min's posttarget pins the standard to C++17, so override it here.
+set_target_properties(${PROJECT_NAME} PROPERTIES
+ CXX_STANDARD 20
+ CXX_STANDARD_REQUIRED ON
+)
+
+# Catch-based unit tests (built when the _test.cpp is present; run via ctest).
+include(${C74_MIN_API_DIR}/test/min-object-unittest.cmake)
diff --git a/source/projects/tap.808.seq_tilde/tap.808.seq_tilde.cpp b/source/projects/tap.808.seq_tilde/tap.808.seq_tilde.cpp
new file mode 100644
index 00000000..0db35d3d
--- /dev/null
+++ b/source/projects/tap.808.seq_tilde/tap.808.seq_tilde.cpp
@@ -0,0 +1,320 @@
+/// @file
+/// @brief tap.808.seq~ — one step-sequencer drum row (the tap.808.* trigger contract).
+/// @details A thin Min wrapper around the shared step-sequencer kernel in step_seq.h
+/// (taptools::seq::trigger_row — the engine, the swing warp, the slot store, and
+/// the pinned trigger levels all live there; see its header for the model and
+/// plans/tap.seq.md for the design of record).
+///
+/// The clocking idiom: the inlet takes a phase ramp, 0..1 per pattern cycle —
+/// connect phasor~ (transport-locked if you like). The row derives its step from
+/// the phase, so every row fed by one phasor stays phase-coherent forever, and
+/// different @length values off the same ramp are polymeter. At each (swung) step
+/// start the row emits an impulse whose amplitude is the step's velocity 0..1 —
+/// the family's amplitude-as-accent trigger, wired straight into any tap.808.*
+/// voice (or tap.adsr~, or anything edge-triggered; @pulse widens the impulse
+/// into a held gate for envelope consumers).
+///
+/// Twelve rows = twelve of these off one phasor, one per voice — the hardware's
+/// row decomposition as patch cords. The accent row falls out: set the same
+/// accents list on every row for the shared-bus feel.
+/// @author Timothy Place
+/// @copyright Copyright 2026 Timothy Place. Distributed under the New BSD License.
+
+#include
+
+#include
+
+#include "c74_min.h"
+
+using namespace c74::min;
+namespace kernel = taptools::seq;
+
+class seq808 : public object, public vector_operator<> {
+ private:
+ // Constructed before the attributes below so attribute setters can forward into it.
+ kernel::trigger_row m_row;
+ bool m_hit[kernel::k_max_steps]{};
+ bool m_flag[kernel::k_max_steps]{};
+ double m_plain{kernel::k_trig_plain};
+ double m_accented{kernel::k_trig_accented};
+ std::atomic m_step_now{-1};
+ int m_step_sent{-1};
+
+ public:
+ MIN_DESCRIPTION{"One step-sequencer drum row for the tap.808 family. Feed a phase ramp "
+ "(phasor~, 0..1 per pattern cycle) and the row emits a trigger impulse at "
+ "each sounding step whose amplitude 0..1 is the accent — exactly the "
+ "tap.808.* trigger contract, modeled on the hardware's common 4-14 V bus. "
+ "Per-step velocities (0 = rest), or the hardware view via hits/accents "
+ "lists mapped onto the plain and accented levels. Sixteen steps by default "
+ "(up to 64 — different lengths off one phasor are polymeter), swing, 16 "
+ "pattern slots with boundary-quantized recall (the A/B-half and fill "
+ "switching), and dictionary save/recall. One row per voice, all off one "
+ "phasor: the 808's sequencer as patch cords."};
+ MIN_TAGS{"sequencing, drums"};
+ MIN_AUTHOR{"Timothy Place"};
+ MIN_RELATED{"tap.303.seq~, tap.808.kick~, tap.808.hat~, tap.adsr~, phasor~"};
+
+ inlet<> m_in{this, "(signal) phase ramp: 0..1 per pattern cycle (connect phasor~)"};
+ outlet<> m_out{this, "(signal) triggers: impulse amplitude 0..1 = accent", "signal"};
+ outlet<> m_out_step{this, "(int) current step number, 1-based, at each step change"};
+
+ seq808(const atoms& args = {}) { m_row.prepare(samplerate()); }
+
+ attribute length{this, "length", kernel::k_default_length, setter{ MIN_FUNCTION {
+ const int v = std::clamp(static_cast(args[0]), 1, kernel::k_max_steps);
+ m_row.clock().data().set_length(v);
+ return {v};
+ }},
+ description{"Steps per pattern cycle (1..64, default 16). Different lengths on rows "
+ "sharing one phasor run in polymeter — length 12 against 16 is the "
+ "hardware's triplet pre-scale and then some."}};
+
+ attribute swing{this, "swing", 0.0, setter{ MIN_FUNCTION {
+ const double v = std::clamp(static_cast(args[0]), 0.0, 1.0);
+ m_row.clock().set_swing(v);
+ return {v};
+ }},
+ description{"Swing 0..1: odd-numbered steps play late by up to half a step "
+ "(0.67 is the classic triplet shuffle). The hardware had none — 0 is "
+ "stock."}};
+
+ attribute plain{this, "plain", kernel::k_trig_plain, setter{ MIN_FUNCTION {
+ m_plain = std::clamp(static_cast(args[0]), 0.002, 1.0);
+ sync_flag_velocities();
+ return {m_plain};
+ }},
+ description{"Trigger level for un-accented hits (0..1 amplitude). The default 0.01 "
+ "is the hardware's 4 V base trigger — accent ~0, kept just above the "
+ "voices' edge threshold."}};
+
+ attribute accented{this, "accented", kernel::k_trig_accented, setter{ MIN_FUNCTION {
+ m_accented = std::clamp(static_cast(args[0]), 0.0, 1.0);
+ sync_flag_velocities();
+ return {m_accented};
+ }},
+ description{"Trigger level for accented hits (0..1 amplitude). The default 0.5 "
+ "is the accent level knob at noon; 1.0 is the full 14 V bus."}};
+
+ attribute pulse{this, "pulse", 0.0, setter{ MIN_FUNCTION {
+ const double v = std::max(0.0, static_cast(args[0]));
+ m_row.set_pulse_ms(v);
+ return {v};
+ }},
+ description{"Impulse width in ms. 0 (default) is a single-sample edge — the family "
+ "contract; longer holds a gate for driving envelopes. Keep it shorter "
+ "than a step, or back-to-back triggers merge."}};
+
+ attribute quantize{this, "quantize", "cycle", setter{ MIN_FUNCTION {
+ const symbol s = args[0];
+ const int m = (s == symbol("now")) ? kernel::quantize_now
+ : (s == symbol("step")) ? kernel::quantize_step
+ : kernel::quantize_cycle;
+ m_row.clock().set_quantize(m);
+ return {m == kernel::quantize_now ? symbol("now")
+ : m == kernel::quantize_step ? symbol("step")
+ : symbol("cycle")};
+ }},
+ description{"When a recalled pattern takes over: cycle (default — the next "
+ "pattern top, the hardware's A/B and fill feel), step, or now."}};
+
+ attribute mute{this, "mute", false, description{"Silence the trigger output (the clock keeps running)."}};
+
+ // -- messages -------------------------------------------------------------------------------------
+
+ message<> velocities{this, "velocities",
+ "Set per-step trigger levels directly: velocities ... (0..1 each, 0 = rest).",
+ MIN_FUNCTION {
+ auto& p = m_row.clock().data();
+ const size_t n = std::min(args.size(), static_cast(kernel::k_max_steps));
+ for (size_t k = 0; k < n; ++k) {
+ const double v = std::clamp(static_cast(args[k]), 0.0, 1.0);
+ p.steps[k].velocity = v;
+ m_hit[k] = v > 0.0;
+ m_flag[k] = v >= m_accented;
+ }
+ return {};
+ }};
+
+ message<> hits{this, "hits",
+ "The hardware view: hits <0/1> ... — which steps sound, at the plain level (accented "
+ "steps use the accented level). Overwrites the row's velocities.",
+ MIN_FUNCTION {
+ const size_t n = std::min(args.size(), static_cast(kernel::k_max_steps));
+ for (size_t k = 0; k < n; ++k) {
+ m_hit[k] = static_cast(args[k]) != 0;
+ }
+ sync_flag_velocities();
+ return {};
+ }};
+
+ message<> accents{this, "accents",
+ "The hardware view: accents <0/1> ... — which sounding steps are accented. "
+ "Overwrites the row's velocities.",
+ MIN_FUNCTION {
+ const size_t n = std::min(args.size(), static_cast(kernel::k_max_steps));
+ for (size_t k = 0; k < n; ++k) {
+ m_flag[k] = static_cast(args[k]) != 0;
+ }
+ sync_flag_velocities();
+ return {};
+ }};
+
+ message<> step{this, "step", "Set one step: step .",
+ MIN_FUNCTION {
+ if (args.size() < 2) {
+ return {};
+ }
+ const int k = static_cast(args[0]) - 1;
+ if (k < 0 || k >= kernel::k_max_steps) {
+ return {};
+ }
+ const double v = std::clamp(static_cast(args[1]), 0.0, 1.0);
+ m_row.clock().data().steps[k].velocity = v;
+ m_hit[k] = v > 0.0;
+ m_flag[k] = v >= m_accented;
+ return {};
+ }};
+
+ message<> store{this, "store", "Store the pattern in a slot (1..16).",
+ MIN_FUNCTION {
+ if (!args.empty()) {
+ m_row.clock().store(static_cast(args[0]) - 1);
+ }
+ return {};
+ }};
+
+ message<> recall{this, "recall",
+ "Recall a slot (1..16); takes over at the quantize boundary (default: the next "
+ "pattern top — the A/B-half / fill switch).",
+ MIN_FUNCTION {
+ if (!args.empty()) {
+ m_row.clock().recall(static_cast(args[0]) - 1);
+ }
+ return {};
+ }};
+
+ message<> todict{this, "todict", "Write the pattern into a named dictionary: todict .",
+ MIN_FUNCTION {
+ if (args.empty()) {
+ return {};
+ }
+ dict d{symbol(args[0])};
+ auto& p = m_row.clock().data();
+ atoms v;
+ for (int k = 0; k < p.length; ++k) {
+ v.push_back(p.steps[k].velocity);
+ }
+ set_key(d, "length", atoms{p.length});
+ set_key(d, "velocities", v);
+ d.touch();
+ return {};
+ }};
+
+ message<> fromdict{this, "fromdict", "Read a pattern written by todict: fromdict .",
+ MIN_FUNCTION {
+ if (args.empty()) {
+ return {};
+ }
+ try {
+ dict d{symbol(args[0])};
+ const atoms ln = d.at("length");
+ const atoms v = d.at("velocities");
+ if (ln.empty() || v.empty()) {
+ throw std::runtime_error("empty");
+ }
+ const int n = std::clamp(static_cast(ln[0]), 1, kernel::k_max_steps);
+ auto& p = m_row.clock().data();
+ p.set_length(n);
+ length = n;
+ for (int k = 0; k < n && k < static_cast(v.size()); ++k) {
+ const double x = std::clamp(static_cast(v[k]), 0.0, 1.0);
+ p.steps[k].velocity = x;
+ m_hit[k] = x > 0.0;
+ m_flag[k] = x >= m_accented;
+ }
+ }
+ catch (...) {
+ cerr << "fromdict: no pattern found in that dictionary" << endl;
+ }
+ return {};
+ }};
+
+ message<> clear{this, "clear", "Empty the pattern (all rests) and reset the clock state.",
+ MIN_FUNCTION {
+ m_row.clock().data().clear();
+ for (auto& h : m_hit) {
+ h = false;
+ }
+ for (auto& f : m_flag) {
+ f = false;
+ }
+ m_row.reset();
+ return {};
+ }};
+
+ message<> dspsetup{this, "dspsetup", "Reconfigure for the sample rate when DSP starts.",
+ MIN_FUNCTION {
+ m_row.prepare(samplerate());
+ return {};
+ }};
+
+ // Step-change notification: coalescing single-value handoff from the audio thread to the
+ // scheduler (the tap.sift~ ring's little sibling — the newest index wins, which is all a
+ // step display wants).
+ timer<> m_notify{this,
+ MIN_FUNCTION {
+ const int k = m_step_now.load(std::memory_order_acquire);
+ if (k >= 0 && k != m_step_sent) {
+ m_step_sent = k;
+ m_out_step.send(k + 1);
+ }
+ return {};
+ }};
+
+ // -- perform --------------------------------------------------------------------------------------
+
+ void operator()(audio_bundle input, audio_bundle output) {
+ const long n = input.frame_count();
+ const double* in = input.samples(0);
+ double* out = output.samples(0);
+ const bool muted = mute;
+
+ if (!m_in.has_signal_connection()) {
+ std::fill_n(out, n, 0.0);
+ return;
+ }
+ for (long i = 0; i < n; ++i) {
+ const double y = m_row.process(in[i]);
+ out[i] = muted ? 0.0 : y;
+ }
+ const int k = m_row.clock().current_step();
+ if (k != m_step_now.load(std::memory_order_relaxed)) {
+ m_step_now.store(k, std::memory_order_release);
+ m_notify.delay(0);
+ }
+ }
+
+ // Exposed for unit tests.
+ kernel::trigger_row& row() { return m_row; }
+
+ private:
+ // Set a list value on a dictionary key (atom_reference only assigns scalars).
+ static void set_key(dict& d, const char* key, const atoms& v) {
+ if (v.empty()) {
+ return;
+ }
+ c74::max::dictionary_appendatoms(reinterpret_cast(static_cast(d)),
+ symbol(key), static_cast(v.size()),
+ reinterpret_cast(const_cast(v.data())));
+ }
+
+ void sync_flag_velocities() {
+ auto& p = m_row.clock().data();
+ for (int k = 0; k < kernel::k_max_steps; ++k) {
+ p.steps[k].velocity = m_hit[k] ? (m_flag[k] ? m_accented : m_plain) : 0.0;
+ }
+ }
+};
+
+MIN_EXTERNAL(seq808);
diff --git a/source/projects/tap.808.seq_tilde/tap.808.seq_tilde_test.cpp b/source/projects/tap.808.seq_tilde/tap.808.seq_tilde_test.cpp
new file mode 100644
index 00000000..3c5acdb6
--- /dev/null
+++ b/source/projects/tap.808.seq_tilde/tap.808.seq_tilde_test.cpp
@@ -0,0 +1,92 @@
+/// @file
+/// @brief Unit tests for tap.808.seq~ (Min-level: attribute defaults, clamping, plumbing).
+// SPDX-License-Identifier: BSD-3-Clause
+// Copyright 2026 Timothy Place.
+
+#include "c74_min_unittest.h" // required unit-test header (defines main via Catch)
+#include "tap.808.seq_tilde.cpp" // include the object source so we can instantiate it
+
+SCENARIO("tap.808.seq~ instantiates with the plan's defaults") {
+ ext_main(nullptr); // configure the class (required once per test executable)
+
+ GIVEN("a default instance") {
+ test_wrapper an_instance;
+ seq808& my_object = an_instance;
+
+ THEN("the pattern is 16 steps, straight, single-sample impulses, cycle-quantized") {
+ REQUIRE(static_cast(my_object.length) == 16);
+ REQUIRE(static_cast(my_object.swing) == 0.0);
+ REQUIRE(static_cast(my_object.pulse) == 0.0);
+ REQUIRE(my_object.quantize == symbol("cycle"));
+ }
+ THEN("the trigger levels are the pinned bus constants") {
+ REQUIRE(static_cast(my_object.plain) == taptools::seq::k_trig_plain);
+ REQUIRE(static_cast(my_object.accented) == taptools::seq::k_trig_accented);
+ }
+ }
+}
+
+SCENARIO("tap.808.seq~ clamps and plumbs into the kernel") {
+ ext_main(nullptr);
+
+ GIVEN("an instance") {
+ test_wrapper an_instance;
+ seq808& my_object = an_instance;
+
+ WHEN("length is set out of range") {
+ my_object.length = 999;
+ THEN("it clamps to the kernel maximum, in the kernel too") {
+ REQUIRE(static_cast(my_object.length) == taptools::seq::k_max_steps);
+ REQUIRE(my_object.row().clock().data().length == taptools::seq::k_max_steps);
+ }
+ }
+ WHEN("swing is set past 1") {
+ my_object.swing = 3.0;
+ THEN("it clamps to 1 and reaches the engine") {
+ REQUIRE(static_cast(my_object.swing) == 1.0);
+ REQUIRE(my_object.row().clock().swing() == 1.0);
+ }
+ }
+ WHEN("a hits list arrives") {
+ my_object.hits(c74::min::atoms{1, 0, 1, 0});
+ THEN("sounding steps carry the plain level and rests are zero") {
+ auto& p = my_object.row().clock().data();
+ REQUIRE(p.steps[0].velocity == taptools::seq::k_trig_plain);
+ REQUIRE(p.steps[1].velocity == 0.0);
+ REQUIRE(p.steps[2].velocity == taptools::seq::k_trig_plain);
+ }
+ }
+ WHEN("an accents list marks a sounding step") {
+ my_object.hits(c74::min::atoms{1, 1});
+ my_object.accents(c74::min::atoms{0, 1});
+ THEN("the accented step carries the accented level") {
+ auto& p = my_object.row().clock().data();
+ REQUIRE(p.steps[0].velocity == taptools::seq::k_trig_plain);
+ REQUIRE(p.steps[1].velocity == taptools::seq::k_trig_accented);
+ }
+ }
+ WHEN("a step message sets a velocity directly") {
+ my_object.step(c74::min::atoms{3, 0.8}); // 1-based
+ THEN("step 3 holds it") {
+ REQUIRE(my_object.row().clock().data().steps[2].velocity == 0.8);
+ }
+ }
+ WHEN("quantize is set to now and a stored slot is recalled") {
+ my_object.step(c74::min::atoms{1, 0.6});
+ my_object.store(c74::min::atoms{1});
+ my_object.step(c74::min::atoms{1, 1.0});
+ my_object.quantize = c74::min::symbol("now");
+ my_object.recall(c74::min::atoms{1});
+ THEN("the slot applies immediately") {
+ REQUIRE(my_object.row().clock().data().steps[0].velocity == 0.6);
+ }
+ }
+ WHEN("clear arrives") {
+ my_object.step(c74::min::atoms{1, 1.0});
+ my_object.clear();
+ THEN("the pattern is all rests") {
+ REQUIRE(my_object.row().clock().data().steps[0].velocity == 0.0);
+ }
+ }
+ }
+}
diff --git a/submodules/taptools b/submodules/taptools
index 6804c91f..fbcf3261 160000
--- a/submodules/taptools
+++ b/submodules/taptools
@@ -1 +1 @@
-Subproject commit 6804c91fd875c6c9660bd65ad72fc5e9edeb7fdc
+Subproject commit fbcf32617d3731b98327d575229298dd946f2159