Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions BLUEPRINT.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ All drum sounds are **synthesized from scratch** — no samples, no external DSP
- Save/Load dialogs (Ctrl+S / Ctrl+O), file picker, dirty flag in title bar
- Activity bar with per-track trigger flash indicators and live parameter display
- Help overlay with 2-column key binding reference (~22 rows, shorthand S-/A-/C- notation)
- **Ableton Link sync** (default-on `link` feature): F3 toggles Link; tempo, start/stop, and beat position are slaved to the network session — multiple TextStep instances or DAWs lock together by bar.

---

Expand All @@ -50,6 +51,7 @@ textstep/
│ │ ├── clock.rs # SequencerClock: sample-accurate step advancement
│ │ ├── drum_voice.rs # DrumVoiceDsp trait + 8 voice implementations + apply_drive()
│ │ ├── effects.rs # ReverbEffect + DelayEffect + GlueCompressor
│ │ ├── link.rs # Ableton Link (gated by `link` feature) — Link-as-master timing
│ │ └── mixer.rs # Mute/solo logic, soft_clip(tanh)
│ │
│ ├── sequencer/
Expand Down Expand Up @@ -82,8 +84,22 @@ cpal = "0.15" # Cross-platform audio (CoreAudio on macOS)
crossbeam-channel = "0.5" # Lock-free bounded channels for UI<->Audio
serde = { version = "1", features = ["derive"] } # Serialization for project files
serde_json = "1" # JSON format for .tsp project files
log = "0.4"
simplelog = "0.12"

# Ableton Link sync (default-enabled via the `link` feature)
ableton-link-rs = { version = "0.1", optional = true }
tokio = { version = "1", features = ["rt", "time"], optional = true }
tracing = { version = "0.1", optional = true }
tracing-subscriber = { version = "0.3", optional = true }

[features]
default = ["link"]
link = ["dep:ableton-link-rs", "dep:tokio", "dep:tracing", "dep:tracing-subscriber"]
```

The `link` feature is on by default. Use `cargo build --no-default-features` to opt out.

---

## 3. Thread Architecture
Expand Down Expand Up @@ -371,6 +387,30 @@ Sample-accurate step timing: `samples_per_step = sample_rate * 60.0 / bpm / 4.0`
- First call after `new()` or `reset()` immediately returns step 0
- Steps wrap at `loop_length` (configurable: 8/16/24/32)
- `StepEvent` includes: `drum_step`, `beat` (0..3), `is_bar_start`
- When Ableton Link is master (see §5.6), `clock.advance()` is bypassed and the clock is re-stamped via `set_step()` / `set_sub_phase()` from `LinkStatus.beat_position` every callback.

### 5.6 Ableton Link (`src/audio/link.rs`)

Gated by the `link` cargo feature (default-on). A dedicated thread owns a `BasicLink` instance on a single-thread tokio runtime and communicates via:

- `LinkCommand` channel (UI → Link thread): `Enable` / `Disable` / `SetTempo(bpm)` / `SetPlaying(bool)`
- `LinkStatus` (shared atomics, lock-free): `enabled`, `num_peers`, `tempo`, `beat_position`, `is_playing`. Read by both the audio callback and UI tick.

**Link-as-master timing.** When Link is enabled and `is_playing`, the audio engine derives the current step every callback:

```
beat_pos = link_status.beat_position() // absolute beats since session start
target_step = floor(beat_pos * 4) % MAX_STEPS // 16th-note resolution
sub_phase = (beat_pos * 4).fract() * samples_per_step
```

The local `SequencerClock` is re-stamped (`set_step` + `set_sub_phase`) — not advanced. This eliminates drift by construction: there is no free-running sample counter while Link is master.

**Quantum-aligned start.** The Link thread asks Link to begin at the next bar (`request_beat_at_start_playing_time(0.0, 4.0)`). The audio thread does *not* call `clock.reset()` on `is_playing` rising edge — it waits for `beat_position >= 0` and lets the per-callback derivation take over. On the falling edge it does reset the clock (preserves stop sound behavior).

**BPM routing.** User-driven BPM changes (keys, scenes, pattern switches) go through `App::set_bpm()`. When Link is enabled it sends `LinkCommand::SetTempo(bpm)` and skips the local write — the new tempo round-trips through Link to all peers and is published back to the audio thread within ~1 ms. The 0.01 BPM threshold is kept only for delay-time recompute (it's expensive); tempo assignment runs every callback.

Full design: `docs/superpowers/specs/2026-05-30-ableton-link-sync-design.md`.

### 5.3 Drum Voices (`src/audio/drum_voice.rs`)

Expand Down
69 changes: 68 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,14 @@ Two-thread model: UI thread (ratatui + crossterm) and audio thread (cpal/CoreAud

#### Audio (`src/audio/`)
- `engine.rs` — audio callback, receives messages, triggers voices
- `clock.rs` — beat/step timing, swing
- `clock.rs` — beat/step timing, swing. When Link is master, `set_step` / `set_sub_phase` are used to re-stamp from `beat_position` every callback instead of `advance()`.
- `drum_voice.rs` — per-track DSP (kick, snare, hats, etc.), params: tune/sweep/color/snap/filter/drive/decay/volume
- `synth_voice.rs` — polyphonic synth DSP, 2 oscillators, 2 envelopes, filter with own ADSR
- `mixer.rs` — channel mixing, send effects
- `effects.rs` — FDN reverb, delay, compressor, saturator, lookahead limiter, sidechain envelope, oversampler
- `display_buffer.rs` — lock-free audio→UI buffer for waveform display
- `fft.rs` — FFT for spectrum analyzer
- `link.rs` — Ableton Link integration (gated by `link` cargo feature). Dedicated thread owns `BasicLink`; UI sends `LinkCommand`s, audio + UI read `LinkStatus` atomics. See `docs/superpowers/specs/2026-05-30-ableton-link-sync-design.md`.

#### Sequencer (`src/sequencer/`)
- `drum_pattern.rs` — `DrumPattern` (8 tracks x 32 steps + 8 `DrumTrackParams`), `TrackId` enum, `NUM_DRUM_TRACKS=8`, `MAX_STEPS=32`
Expand All @@ -68,6 +69,9 @@ Two-thread model: UI thread (ratatui + crossterm) and audio thread (cpal/CoreAud
### Layout Must Match in Two Places
Render layout (`ui/mod.rs`) and mouse hit-testing (`mouse.rs`) must compute identical layouts. Always use constants from `ui/layout.rs`. If you change a height/width in rendering, update the corresponding mouse hit-test.

### Link Is the Clock When Enabled
When the `link` feature is enabled and a Link session is active, `LinkStatus.beat_position` is the source of truth — the local `SequencerClock` is re-stamped every callback via `set_step` / `set_sub_phase`. Do not call `clock.advance()` while Link is master, and do not write `transport.bpm` / `transport.state` from the UI thread (audio thread is sole writer). User-driven BPM changes go through `App::set_bpm()`, which routes to `LinkCommand::SetTempo` when Link is on.

### Synth Knobs Grouped Layout
`synth_knobs.rs` uses percentage-based `Layout::split()` for side-by-side sections. `mouse.rs` replicates the exact same splits. If you add/remove a synth param group, update both files.

Expand All @@ -93,3 +97,66 @@ Render layout (`ui/mod.rs`) and mouse hit-testing (`mouse.rs`) must compute iden

### Data Directory
`~/Library/Application Support/textstep/` (macOS). Projects saved as `.tsp` (JSON).

# context-mode — MANDATORY routing rules

You have context-mode MCP tools available. These rules are NOT optional — they protect your context window from flooding. A single unrouted command can dump 56 KB into context and waste the entire session.

## BLOCKED commands — do NOT attempt these

### curl / wget — BLOCKED
Any Bash command containing `curl` or `wget` is intercepted and replaced with an error message. Do NOT retry.
Instead use:
- `ctx_fetch_and_index(url, source)` to fetch and index web pages
- `ctx_execute(language: "javascript", code: "const r = await fetch(...)")` to run HTTP calls in sandbox

### Inline HTTP — BLOCKED
Any Bash command containing `fetch('http`, `requests.get(`, `requests.post(`, `http.get(`, or `http.request(` is intercepted and replaced with an error message. Do NOT retry with Bash.
Instead use:
- `ctx_execute(language, code)` to run HTTP calls in sandbox — only stdout enters context

### WebFetch — BLOCKED
WebFetch calls are denied entirely. The URL is extracted and you are told to use `ctx_fetch_and_index` instead.
Instead use:
- `ctx_fetch_and_index(url, source)` then `ctx_search(queries)` to query the indexed content

## REDIRECTED tools — use sandbox equivalents

### Bash (>20 lines output)
Bash is ONLY for: `git`, `mkdir`, `rm`, `mv`, `cd`, `ls`, `npm install`, `pip install`, and other short-output commands.
For everything else, use:
- `ctx_batch_execute(commands, queries)` — run multiple commands + search in ONE call
- `ctx_execute(language: "shell", code: "...")` — run in sandbox, only stdout enters context

### Read (for analysis)
If you are reading a file to **Edit** it → Read is correct (Edit needs content in context).
If you are reading to **analyze, explore, or summarize** → use `ctx_execute_file(path, language, code)` instead. Only your printed summary enters context. The raw file content stays in the sandbox.

### Grep (large results)
Grep results can flood context. Use `ctx_execute(language: "shell", code: "grep ...")` to run searches in sandbox. Only your printed summary enters context.

## Tool selection hierarchy

1. **GATHER**: `ctx_batch_execute(commands, queries)` — Primary tool. Runs all commands, auto-indexes output, returns search results. ONE call replaces 30+ individual calls.
2. **FOLLOW-UP**: `ctx_search(queries: ["q1", "q2", ...])` — Query indexed content. Pass ALL questions as array in ONE call.
3. **PROCESSING**: `ctx_execute(language, code)` | `ctx_execute_file(path, language, code)` — Sandbox execution. Only stdout enters context.
4. **WEB**: `ctx_fetch_and_index(url, source)` then `ctx_search(queries)` — Fetch, chunk, index, query. Raw HTML never enters context.
5. **INDEX**: `ctx_index(content, source)` — Store content in FTS5 knowledge base for later search.

## Subagent routing

When spawning subagents (Agent/Task tool), the routing block is automatically injected into their prompt. Bash-type subagents are upgraded to general-purpose so they have access to MCP tools. You do NOT need to manually instruct subagents about context-mode.

## Output constraints

- Keep responses under 500 words.
- Write artifacts (code, configs, PRDs) to FILES — never return them as inline text. Return only: file path + 1-line description.
- When indexing content, use descriptive source labels so others can `ctx_search(source: "label")` later.

## ctx commands

| Command | Action |
|---------|--------|
| `ctx stats` | Call the `ctx_stats` MCP tool and display the full output verbatim |
| `ctx doctor` | Call the `ctx_doctor` MCP tool, run the returned shell command, display as checklist |
| `ctx upgrade` | Call the `ctx_upgrade` MCP tool, run the returned shell command, display as checklist |
Loading
Loading