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
2 changes: 1 addition & 1 deletion .cursor/rules/architecture-principles.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Conventions established by [docs/architecture-refactor.md](docs/architecture-ref
- **Single source for persisted config.** New persisted fields go through `persisted_session_payload` in [cleave/config_schema.py](cleave/config_schema.py) so load, save, and dirty tracking stay aligned; do not add a parallel serializer.
- **Dirty tracking is computed, not marked.** Mutate session state and let the signature compare detect changes; never reintroduce `mark_config_dirty()`. See [cleave/config_snapshot.py](cleave/config_snapshot.py) (`persisted_session_signature`).
- **Thin controllers, split by feature.** [cleave/viz/controls.py](cleave/viz/controls.py) `TuningControls` coordinates focus and input and delegates to [cleave/viz/config_save.py](cleave/viz/config_save.py), [cleave/viz/render_overlay_controls.py](cleave/viz/render_overlay_controls.py), [cleave/viz/render_post_fx_controls.py](cleave/viz/render_post_fx_controls.py); do not let one class accumulate unrelated responsibilities.
- **Model, controller, and view separate.** Session dataclasses in [cleave/viz/session.py](cleave/viz/session.py); view model via `TuningViewStateBuilder` in [cleave/viz/tuning_view_state.py](cleave/viz/tuning_view_state.py); [cleave/viz/overlay.py](cleave/viz/overlay.py) only draws.
- **Model, controller, and view separate.** Session dataclasses in [cleave/viz/session.py](cleave/viz/session.py); view model via `TuningViewStateBuilder` in [cleave/viz/tuning_view_state.py](cleave/viz/tuning_view_state.py); [cleave/viz/tuning_panel_draw.py](cleave/viz/tuning_panel_draw.py) draws the live tuning panel.
- **Readiness enforced with types.** Prefer `VisualizerSeed`, `VisualizerCore`, `LiveVisualizerRuntime`, and `RenderVisualizerRuntime` in [cleave/viz/app.py](cleave/viz/app.py) over optional fields guarded by `assert ... is not None`. Core composes seed via `runtime.seed.*`.
- **Typed dependency injection.** Pass [cleave/viz/focus_context.py](cleave/viz/focus_context.py) `FocusContext` (or similar small typed context), never a dict of lambdas or `setattr` by string.
- **Public APIs across modules.** No cross-module underscore imports; expose helpers on a class or module if another package needs them.
Expand Down
12 changes: 6 additions & 6 deletions .cursor/rules/live-tuning-ui.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ alwaysApply: false

# Live tuning overlay layout

The focus-driven tree panel ([cleave/viz/overlay.py](cleave/viz/overlay.py) draws; [cleave/viz/controls.py](cleave/viz/controls.py) coordinates input; [cleave/viz/tuning_view_state.py](cleave/viz/tuning_view_state.py) builds `TuningViewState`; [cleave/viz/session.py](cleave/viz/session.py) holds session) splits into two vertical sections. Use these names in docs, issues, and UI work.
The focus-driven tree panel ([cleave/viz/tuning_panel_draw.py](cleave/viz/tuning_panel_draw.py) draws; [cleave/viz/controls.py](cleave/viz/controls.py) coordinates input; [cleave/viz/tuning_view_state.py](cleave/viz/tuning_view_state.py) builds `TuningViewState`; [cleave/viz/row_layout.py](cleave/viz/row_layout.py) builds `RowLayout`; [cleave/viz/session.py](cleave/viz/session.py) holds session) splits into two vertical sections. Use these names in docs, issues, and UI work.

Unsaved config edits show an asterisk on the active config path row. **Ctrl+Q** and window close route through `TuningControls.try_quit()` (delegates to [cleave/viz/config_save.py](cleave/viz/config_save.py)); when dirty, a centered three-option quit modal (Save / Don't save / Cancel) appears before exit.

Expand All @@ -16,9 +16,9 @@ Confirm, save-choice, and unsaved-quit prompts are drawn by [cleave/viz/modal_ov

## Track rows section

The scrollable block below the pinned header: all rows where `row_kind` is not in `HEADER_ROW_KINDS` from [cleave/viz/row_semantics.py](cleave/viz/row_semantics.py) (`track_row_count` counts these rows from `build_row_layout`). Row interaction groups (sub-row frozensets, repeat keys, help affordances) are defined in the same module.
The scrollable block below the pinned header: all rows where `row_kind` is not in `HEADER_ROW_KINDS` from [cleave/viz/row_semantics.py](cleave/viz/row_semantics.py) (`state.layout.track_row_count()` counts these rows). Row interaction groups (sub-row frozensets, repeat keys, help affordances) are defined in the same module.

- One **track block** per slot in `layer_z_order` (dynamic slots `layer_1`..`layer_8`; default four; use `row_slot()` in [cleave/viz/overlay.py](cleave/viz/overlay.py) to read the slot from a row index).
- One **track block** per slot in `layer_z_order` (dynamic slots `layer_1`..`layer_8`; default four; use `state.layout.slot(index)` in [cleave/viz/row_layout.py](cleave/viz/row_layout.py) to read the slot from a row index).
- **Base rows** (always present per stem): header, preset dir, preset, stem, blend, opacity, beat sensitivity, then **cleave effects** header (`RowKind.TRACK_EFFECTS_HEADER`).
- **Stem row** (`RowKind.TRACK_STEM`): Left/Right cycles `STEM_SOURCES`; follows standard locked sub-row rules (blocked when layer locked; only **cleave effects** header remains navigable among locked sub-rows).
- **Effect sub-rows** (`RowKind.TRACK_EFFECT`): one per entry in [cleave/effects/registry.py](cleave/effects/registry.py) for that stem; visible only when both the track block and `TrackBlock.effects_expanded` are expanded.
Expand All @@ -37,14 +37,14 @@ Below post-FX, **Render: TIMELINE** (`RowKind.RENDER_TIMELINE_HEADER`) is always

## Timeline panel

Separate bottom overlay ([cleave/viz/timeline_overlay.py](cleave/viz/timeline_overlay.py), [cleave/viz/timeline_controls.py](cleave/viz/timeline_controls.py)). Open with **t** (opens strip and enters submenu on row 0) or **Right** on **Render: TIMELINE** when `timeline.enabled` (strip only; **Down** enters submenu). **Left** on that header or **t** when the strip is open closes it and returns focus to **Render: TIMELINE**. Strip open does not hide the main overlay or route keys until `submenu_focused` (**t** from the main tree sets this immediately; **Down** from the header when the strip is already open). When the strip is open and enabled, **Up**/**Down** treat timeline rows as part of the main focus ring: order is main navigable rows (ending at **Render: TIMELINE**), then timeline rows 0..N-1, wrapping **Down** from the last timeline row to **TRANSPORT** and **Up** from **TRANSPORT** to the last timeline row; **Up** at timeline row 0 returns focus to **Render: TIMELINE** without closing the strip. **Up**/**Down** always route through the main tuning controls; other timeline keys route to the timeline strip when `submenu_focused`. **Esc** or **t** while in the submenu closes the strip and returns focus to **Render: TIMELINE**. Rows follow `layer_z_order`; labels use stem abbreviations (D/B/V/O). Each row shows a monitor eye beside the label, the cue bar, and a committed-timeline eye at the far right: **left** = monitor/output (`effective_layer_enabled`; gold `OVERRIDE_BG` when stem is in `TimelineRuntime.override_stems` or when recording and the row is armed). Armed rows during record use `record_baseline` + `record_buffer` toggles only, not committed cues. Record start baseline is not drawn on the bar; only transition cues in `record_buffer` show ticks., **right** = committed cues at playhead only (`timeline_committed_visible`; updates on seek while paused preview leaves left eye on `TimelineRuntime.monitor`). **Enter** arms a row (`ARMED_BG` red fill, distinct from override eye). **Space** pause snapshots current output into `monitor` and sets `preview_active` (resume clears both); while recording and playing, **Space** stops the take and pauses instead. Num keys **1**-**8** (main row and numpad) toggle layer visibility while paused (preview via `monitor` or override via `override_visible`; adds stem to override when needed), write record buffer when recording (armed only), toggle `override_visible` while playing for stems in `override_stems` only. **Shift+Enter** toggles override on the focused row while playing or paused (manual override via `override_stems` / `override_visible`; clears preview on enter; distinct from main-panel `session.solo_slot`; ignored when recording). Timeline strip does not use **Shift+Left** / **Shift+Right** for solo (transport seek). **r** start captures WYSIWYG baseline for armed stems, preserves override on unarmed stems, clears preview, unpauses if needed; stop punch-overwrites armed range (playback keeps running). **Ctrl+Space** starts record the same way; while recording it stops the take and pauses (no preview). `preview_active` / `monitor` / `override_stems` / `override_visible` are session-only on `TimelineRuntime`. Cue list persists under root `timeline:` in YAML (written last in snapshots). See [docs/timeline-idea.md](docs/timeline-idea.md).
Separate bottom overlay ([cleave/viz/timeline_overlay.py](cleave/viz/timeline_overlay.py), [cleave/viz/timeline_controls.py](cleave/viz/timeline_controls.py)). Open with **t** (opens strip and enters submenu on row 0) or **Right** on **Render: TIMELINE** when `timeline.enabled` (strip only; **Down** enters submenu). **Left** on that header or **t** when the strip is open closes it and returns focus to **Render: TIMELINE**. Strip open does not hide the main overlay or route keys until `submenu_focused` (**t** from the main tree sets this immediately; **Down** from the header when the strip is already open). When the strip is open and enabled, **Up**/**Down** use one focus ring ([cleave/viz/focus_nav.py](cleave/viz/focus_nav.py)): main navigable rows (ending at **Render: TIMELINE**), then timeline rows 0..N-1, with uniform modulo wrap. **Down** from the last timeline row wraps to **Settings**; **Up** from **Settings** wraps to the last timeline row. **Up** at timeline row 0 returns focus to **Render: TIMELINE** without closing the strip. **Up**/**Down** always route through the main tuning controls; other timeline keys route to the timeline strip when `submenu_focused`. **Esc** or **t** while in the submenu closes the strip and returns focus to **Render: TIMELINE**. Rows follow `layer_z_order`; labels use stem abbreviations (D/B/V/O). Each row shows a monitor eye beside the label, the cue bar, and a committed-timeline eye at the far right: **left** = monitor/output (`effective_layer_enabled`; gold `OVERRIDE_BG` when stem is in `TimelineRuntime.override_stems` or when recording and the row is armed). Armed rows during record use `record_baseline` + `record_buffer` toggles only, not committed cues. Record start baseline is not drawn on the bar; only transition cues in `record_buffer` show ticks., **right** = committed cues at playhead only (`timeline_committed_visible`; updates on seek while paused preview leaves left eye on `TimelineRuntime.monitor`). **Enter** arms a row (`ARMED_BG` red fill, distinct from override eye). **Space** pause snapshots current output into `monitor` and sets `preview_active` (resume clears both); while recording and playing, **Space** stops the take and pauses instead. Num keys **1**-**8** (main row and numpad) toggle layer visibility while paused (preview via `monitor` or override via `override_visible`; adds stem to override when needed), write record buffer when recording (armed only), toggle `override_visible` while playing for stems in `override_stems` only. **Shift+Enter** toggles override on the focused row while playing or paused (manual override via `override_stems` / `override_visible`; clears preview on enter; distinct from main-panel `session.solo_slot`; ignored when recording). Timeline strip does not use **Shift+Left** / **Shift+Right** for solo (transport seek). **r** start captures WYSIWYG baseline for armed stems, preserves override on unarmed stems, clears preview, unpauses if needed; stop punch-overwrites armed range (playback keeps running). **Ctrl+Space** starts record the same way; while recording it stops the take and pauses (no preview). `preview_active` / `monitor` / `override_stems` / `override_visible` are session-only on `TimelineRuntime`. Cue list persists under root `timeline:` in YAML (written last in snapshots). See [docs/timeline-idea.md](docs/timeline-idea.md).

## Header rows section

The pinned top block before a visual gap (`header_gap` in `draw()`): `header_row_count(state)` rows (3 when settings collapsed, 4 when expanded).

- Row kinds: `RowKind.SETTINGS_HEADER`, optional `RowKind.SETTINGS_RENDER_MODE` when `session.settings.expanded`, `RowKind.CONFIG_HEADER` (active config path; Enter saves), `RowKind.TRANSPORT`.
- Layout order in `build_row_layout`: settings header, optional render-mode sub-row, config header, transport.
- Layout order in `RowLayout.build`: settings header, optional render-mode sub-row, config header, transport.
- **Settings** header: label `Settings` in `ACTION`, expand arrow in value color; Left/Right toggles `session.settings.expanded` (UI-only). Sub-row when expanded: `└─ render mode: {mode}` cycles `visualizer.render_mode` in config (`full-quality`, `balanced`, `performance`; default `balanced`).
- Enter on `CONFIG_HEADER` opens an `OVERWRITE` / `SAVE AS NEW` centered modal when overwrite is allowed (`allow_overwrite`; false for repo-root `cleave-viz.yaml`). When overwrite is not allowed, a `SAVE AS NEW` / `CANCEL` modal appears (Enter confirms the focused option, Esc dismisses). Overwrite still uses the yes/no centered modal.

Expand All @@ -70,7 +70,7 @@ Colors live in [cleave/viz/theme.py](cleave/viz/theme.py). Label truncation uses

**Expand arrows** (`▶` / `▼`): value role, not label. Drawn in the row value color (typically `VALUE`; follows focus, disabled, and locked state like other values).

**Visibility eye** (track, render overlay, render post-FX, and render timeline headers): `VALUE` when enabled, `DISABLED` when off. When soloed (`solo_slot` for main-panel layer slots via `TuningViewState.solo_slot`, `render_overlay_solo` / `render_post_fx_solo` for render blocks; neither persisted), the eye stays white on `SOLO_BG`. **Shift + Right** enters solo; **Shift + Left** exits solo for the focused header. **Render: TIMELINE** header has no solo; the timeline strip uses **Shift+Enter** on the focused row for override (`override_stems` on `TimelineRuntime`; left eye `OVERRIDE_BG`). Timeline strip eyes use the same icon helper ([render_visibility_icon](cleave/viz/overlay.py)).
**Visibility eye** (track, render overlay, render post-FX, and render timeline headers): `VALUE` when enabled, `DISABLED` when off. When soloed (`solo_slot` for main-panel layer slots via `TuningViewState.solo_slot`, `render_overlay_solo` / `render_post_fx_solo` for render blocks; neither persisted), the eye stays white on `SOLO_BG`. **Shift + Right** enters solo; **Shift + Left** exits solo for the focused header. **Render: TIMELINE** header has no solo; the timeline strip uses **Shift+Enter** on the focused row for override (`override_stems` on `TimelineRuntime`; left eye `OVERRIDE_BG`). Timeline strip eyes use the same icon helper ([render_visibility_icon](cleave/viz/tuning_panel_draw.py)).

### Accent colors (not label/value roles)

Expand Down
2 changes: 1 addition & 1 deletion .cursor/rules/project-context.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ alwaysApply: true

# Cleave project context

- BAU iterative development. Visualizer: `python -m cleave play` via [cleave/cli.py](cleave/cli.py) calling `cleave.viz.launch()`; [cleave.py](cleave.py) is an alias; implementation in [cleave/viz/](cleave/viz/) (up to eight Milkdrop/libprojectM layers, default four; add/remove in live tuning, 1280x720 default resolution, live at display frame rate, offline render fps via `render.fps`, stem PCM). Black-key stack in [cleave/gl_compositor.py](cleave/gl_compositor.py). Live tuning: session in [cleave/viz/session.py](cleave/viz/session.py), input in [cleave/viz/controls.py](cleave/viz/controls.py), view state in [cleave/viz/tuning_view_state.py](cleave/viz/tuning_view_state.py), overlay draw in [cleave/viz/overlay.py](cleave/viz/overlay.py); shared live/offline frame finish in [cleave/viz/frame_finish.py](cleave/viz/frame_finish.py). Render credits overlay in [cleave/viz/render_overlay.py](cleave/viz/render_overlay.py) (`render.overlay` in YAML): live preview in play, burned in by [cleave/viz/render.py](cleave/viz/render.py) offline. Cleave effects: [cleave/effects/](cleave/effects/) (dispatch via [cleave/effects/handlers.py](cleave/effects/handlers.py)). Paths: [cleave/paths.py](cleave/paths.py) (repo-root data dir by default, `projects/<slug>/`; `CLEAVE_DATA` override). Config: parse and defaults in [cleave/config_schema.py](cleave/config_schema.py); repo-root [cleave-viz.yaml](cleave-viz.yaml) copied into projects; `unnamed-N.yaml` snapshots via [cleave/config_snapshot.py](cleave/config_snapshot.py). Shared easing: [cleave/easing.py](cleave/easing.py). Architecture conventions: [.cursor/rules/architecture-principles.mdc](.cursor/rules/architecture-principles.mdc).
- BAU iterative development. Visualizer: `python -m cleave play` via [cleave/cli.py](cleave/cli.py) calling `cleave.viz.launch()`; [cleave.py](cleave.py) is an alias; implementation in [cleave/viz/](cleave/viz/) (up to eight Milkdrop/libprojectM layers, default four; add/remove in live tuning, 1280x720 default resolution, live at display frame rate, offline render fps via `render.fps`, stem PCM). Black-key stack in [cleave/gl_compositor.py](cleave/gl_compositor.py). Live tuning: session in [cleave/viz/session.py](cleave/viz/session.py), input in [cleave/viz/controls.py](cleave/viz/controls.py), view state in [cleave/viz/tuning_view_state.py](cleave/viz/tuning_view_state.py), panel draw in [cleave/viz/tuning_panel_draw.py](cleave/viz/tuning_panel_draw.py); layout in [cleave/viz/row_layout.py](cleave/viz/row_layout.py); shared live/offline frame finish in [cleave/viz/frame_finish.py](cleave/viz/frame_finish.py). Render credits overlay in [cleave/viz/render_overlay.py](cleave/viz/render_overlay.py) (`render.overlay` in YAML): live preview in play, burned in by [cleave/viz/render.py](cleave/viz/render.py) offline. Cleave effects: [cleave/effects/](cleave/effects/) (dispatch via [cleave/effects/handlers.py](cleave/effects/handlers.py)). Paths: [cleave/paths.py](cleave/paths.py) (repo-root data dir by default, `projects/<slug>/`; `CLEAVE_DATA` override). Config: parse and defaults in [cleave/config_schema.py](cleave/config_schema.py); repo-root [cleave-viz.yaml](cleave-viz.yaml) copied into projects; `unnamed-N.yaml` snapshots via [cleave/config_snapshot.py](cleave/config_snapshot.py). Shared easing: [cleave/easing.py](cleave/easing.py). Architecture conventions: [.cursor/rules/architecture-principles.mdc](.cursor/rules/architecture-principles.mdc).
- Must-do list: [docs/todos.md](docs/todos.md). Aspirational: [docs/roadmap.md](docs/roadmap.md).
- See [README.md](README.md) for usage.
- Project layout: `projects/<slug>/` under repo root (or `CLEAVE_DATA` override) with copied mix audio, `project.yaml`, `signals.json`, `stems/` (four stem wavs), and optional configs. `separate` copies the source file, writes the manifest, runs Demucs, and writes `signals.json`; the visualizer reads the mix from `project.yaml`. CLI: `python -m cleave separate|play` (or `python cleave.py`, same entry point).
Expand Down
Loading
Loading