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
236 changes: 86 additions & 150 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,162 +2,98 @@

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Halo is a 2-deck DJ app for macOS with an integrated lighting console (Art-Net output driven by per-track cues
and a live programmer). `ROADMAP.md` is the authoritative description of the architecture and feature plan —
read it before designing anything non-trivial.

## Common Commands

### Building and Running
- **Build**: `cargo build --release`
- **Run with Art-Net broadcast**: `cargo run --release -- --source-ip <SOURCE_IP>`
- **Run with unicast and MIDI**: `cargo run --release -- --source-ip 192.168.1.100 --dest-ip 192.168.1.200 --enable-midi`
- **Run with multi-destination setup**: `cargo run --release -- --source-ip 192.168.1.100 --lighting-dest-ip 192.168.1.200 --pixel-dest-ip 192.168.1.201`
- **Load a show file**: `cargo run --release -- --source-ip <SOURCE_IP> --show-file shows/Jasons40th.json`

### CLI Arguments
- `--source-ip <IP>` - Art-Net source IP address (required)
- `--dest-ip <IP>` - Single destination IP (legacy, optional)
- `--lighting-dest-ip <IP>` - Lighting fixtures destination IP (multi-destination)
- `--pixel-dest-ip <IP>` - Pixel fixtures destination IP (multi-destination)
- `--lighting-universe <NUM>` - Universe for lighting fixtures (default: 1)
- `--pixel-start-universe <NUM>` - Starting universe for pixel fixtures (default: 2)
- `--artnet-port <PORT>` - Art-Net port (default: 6454)
- `--broadcast` - Force broadcast mode
- `--enable-midi` - Enable MIDI support
- `--show-file <PATH>` - Path to show JSON file

See `docs/multi-destination-artnet.md` for detailed multi-destination Art-Net setup.

### Development Tools
- **Run**: `cargo run --release` (optionally pass an audio file path as the only positional arg to load it on launch)
- **Check compilation**: `cargo check --workspace --all-targets`
- **Format code**: `cargo +nightly fmt --all` (requires nightly toolchain for unstable formatting options)
- **Lint**: `cargo clippy --workspace --all-targets --all-features -- -D warnings`
- **Format code**: `cargo +nightly fmt --all` (nightly required — `rustfmt.toml` uses unstable options)
- **Test**: `cargo test --workspace`
- **Install nightly toolchain**: `rustup toolchain install nightly` (required for formatting only)
- **Lint**: `cargo clippy --workspace --all-targets` (currently not enforced in CI)
- **macOS app bundle**: `cargo bundle --release` (requires `cargo-bundle`)

There are no CLI flags. Art-Net destination, audio device, and other settings are configured in-app and
persisted (see Persistence below). Logging is controlled with `RUST_LOG`.

### Toolchain

### Toolchain Requirements
- **Stable Rust**: Used for building, testing, and linting (MSRV: 1.90.0)
- **Nightly Rust**: Required only for formatting due to unstable options in `rustfmt.toml`
- **Stable Rust** for building/testing (CI pins 1.90.0); **nightly** only for formatting.
- The `timestretch` crate is a **path dependency on a sibling clone**: `../timestretch-rs` next to this repo
(from `crates/halo`, `path = "../../../timestretch-rs"`). CI checks out `robmorgan/timestretch-rs` to that
location. Builds fail without it.

## Architecture Overview

Halo is a real-time lighting console built with Rust, designed for solo performers. It uses a multi-crate workspace architecture with async/tokio runtime:

### Core Crates
- **`halo-core`**: Core lighting engine with async module system, DMX output, cue system, effects engine, pixel engine, and MIDI integration
- **`halo-fixtures`**: Fixture library and management system
- **`halo-ui`**: egui-based UI components and interface
- **`halo`**: CLI application and main entry point (uses `#[tokio::main]` async runtime)

### Key Systems

#### Lighting Console (`halo-core/src/console.rs`)
- Central `LightingConsole` struct manages all lighting operations
- Uses async `ModuleManager` to coordinate separate modules (`DmxModule`, `AudioModule`, `MidiModule`, `SmpteModule`)
- Channel-based communication using `ConsoleCommand` and `ConsoleEvent` via tokio `mpsc`
- Handles fixture patching, cue management, and MIDI integration
- Supports multi-destination Art-Net routing for different fixture types

#### Cue System (`halo-core/src/cue/`)
- `CueManager` handles playback state and timecode synchronization
- Supports both internal and external SMPTE timecode
- `CueList` contains sequences of `Cue` objects with static values and effects
- Audio playback synchronization with Ableton Link

#### Effect Engine (`halo-core/src/effect/`)
- Mathematical effect generators: sine, sawtooth, square waves
- Beat-synchronized effects using rhythm detection
- Effect distribution across multiple fixtures with customizable parameters

#### MIDI Integration (`halo-core/src/midi/`)
- MPK49 controller support for live performance
- `MidiOverride` system for real-time control during shows
- Actions include static values and cue triggering

#### Module System (`halo-core/src/modules/`)
- `ModuleManager` coordinates all async modules in separate tokio tasks
- Each module implements `AsyncModule` trait with `initialize()`, `run()`, and `shutdown()` methods
- Inter-module communication via `ModuleEvent` (DMX output, audio commands, timecode sync, MIDI input)
- Status/error reporting via `ModuleMessage` back to manager
- **DmxModule**: Real-time DMX output at 44Hz with multi-destination Art-Net routing
- **AudioModule**: Audio file playback in dedicated OS thread (not tokio task) using `rodio` and `symphonia`
- **MidiModule**: MIDI input handling and event forwarding
- **SmpteModule**: SMPTE timecode synchronization for external timecode sources

#### Pixel Engine (`halo-core/src/pixel/`)
- `PixelEngine` manages all pixel bar fixtures with per-universe routing
- Pixel effects: `Chase`, `Wave`, `Strobe`, `ColorCycle`
- `PixelEffectScope` controls effect application: `Bar` (uniform) or `Individual` (per-pixel)
- Beat-synchronized effects using rhythm detection
- Supports effect distribution across multiple fixtures (All, Step, Wave)
- Renders RGB data per universe for pixel bar fixtures

#### Configuration System (`halo-core/src/config.rs`)
- `ConfigManager` handles persistent configuration in `config.json` (repository root)
- `Settings` structure stores audio device, MIDI device, DMX settings, and fixture library preferences
- Configuration loaded at startup; CLI arguments override saved settings
- Settings UI panel allows runtime configuration changes
- Version-aware configuration with migration support

#### UI Architecture (`halo-ui/src/`)
- `HaloApp` is the main egui application with tabbed interface
- Separate panels: Dashboard, Programmer, Cue Editor, Patch Panel, Show Manager
- Real-time fixture grid visualization and control
- Timeline view for cue sequencing

### Network Configuration
- Multi-destination Art-Net architecture via `NetworkConfig` (`artnet/network_config.rs`)
- Multiple `ArtNetDestination` entries with independent broadcast/unicast modes
- Universe routing via `HashMap<u8, usize>` maps universes to destination indices
- Common setup: Universe 1 for lighting fixtures, Universes 2+ for pixel fixtures
- CLI supports separate `--lighting-dest-ip` and `--pixel-dest-ip` for easy multi-destination setup
- Legacy `--dest-ip` still supported for single-destination backward compatibility
- Art-Net output on port 6454 (configurable via `--artnet-port`)

### Show File Format
- JSON-based show files in `shows/` directory
- Contains cue lists with timing, effects, pixel effects, and fixture assignments
- Includes audio file paths for synchronized playback
- Loadable at runtime via `--show-file` parameter
- Managed through UI Show Manager with save/load functionality

## Development Notes

### Repository Conventions

#### Rust Crates
- Crate names are prefixed with `halo-`. For example, the `core` folder's crate is named `halo-core`
- When using `format!` and you can inline variables into `{}`, always do that
- Never use `unsafe` blocks or functions in any code

#### Code Formatting
After making any changes to Rust code, always run:
```bash
cargo +nightly fmt --all
```

### macOS Platform Requirement
- Development and execution require macOS due to system-specific audio and MIDI dependencies
- Uses Core Audio frameworks through `rodio` and `midir` crates

### Async Architecture
- Built on tokio async runtime with async/await throughout the codebase
- Module system uses async tasks for concurrent operation
- Channel-based communication between UI and console (`tokio::sync::mpsc`)
- AudioModule uses dedicated OS thread (not async task) for real-time audio playback

### Configuration Management
- `config.json` file in repository root stores persistent settings
- Auto-created with defaults on first run if not present
- CLI arguments override configuration file settings
- Editable through Settings panel in UI

### Fixture Patching
- Fixtures are defined in the fixture library with channel layouts
- Traditional lighting fixtures typically on Universe 1
- Pixel bar fixtures on Universes 2+ with per-universe routing
- DMX addressing starts from specified universe and channel

### Performance Considerations
- DMX module outputs at 44Hz for smooth lighting output
- Async module architecture allows concurrent operation of DMX, audio, MIDI, and timecode
- UI runs on main thread with egui's native event loop and repaint system
- Channel-based communication between UI and console for thread-safe operation
- Audio playback in dedicated OS thread for consistent timing
Two workspace crates:

- **`crates/halo`** — the application: UI (egui/eframe, wgpu backend), audio engine, decks, mixer DSP, track
library, analysis workers, and the DMX engine thread.
- **`crates/halo-light`** — UI-free, audio-free lighting domain library: fixtures, cues, programmer resolution,
DMX rendering, Art-Net transport.

### Threading model (no tokio)

Plain threads + channels/atomics, mirroring the `timestretch` controller/processor/source split:

- **UI thread** — egui at ~30 fps while playing. Talks to audio via atomics and the engine's wait-free control
mailbox; a mutex only guards cold UI state.
- **Feed/control thread per deck** — keeps the engine's source ring fed, handles warm-start seeks and gapless
loop wraps (`JumpMap` re-anchor), publishes the playhead.
- **Audio callback thread (cpal)** — owns both `EngineProcessor`s and the whole mixer chain
(engine → trim → EQ → filter → fader → crossfader → master → limiter). Must stay **allocation-free and
lock-free**; parameters arrive via atomics and are smoothed per block.
- **Worker threads** (`std::thread` + `mpsc`, drained per frame by the UI) — library import, track analysis.
- **DMX engine thread** — 44 Hz tick: reads deck playheads from atomics, `resolve()` → `render()` → Art-Net send.

### Key modules — `crates/halo/src`

- `main.rs` — eframe/wgpu entry point; positional file arg; `env_logger`.
- `app.rs` — `HaloApp`: views (Prepare/Perform), deck UI, mixer UI, rig ownership, PATCH tab, settings window.
- `deck.rs` — `Deck`: one `timestretch::Engine` per deck, feed thread, seeks, loops, EOF, playhead.
- `audio.rs` — cpal stream setup and the audio callback owning the mixer chain.
- `dsp.rs` — `IsolatorEq` (LR4 crossover, full-kill), `DjFilter` (RBJ biquad LP/HP), `Limiter`.
- `state.rs` — shared atomics (`DeckShared`, `MixerShared`), scrub state, meters, CPU load.
- `scrub.rs` — audible scrub: varispeed voice with momentum glide/settle, engine↔voice crossfade.
- `waveform/` — 3-band RGB peaks pyramid, overview strip, zoomed beat-grid view, trigger-lane strips + cue editor.
- `decoder.rs` — symphonia decode (mp3/flac/ogg/wav) to interleaved stereo `f32`.
- `worker.rs` — background import/analysis workers (own DB connections).
- `library.rs` — SQLite (rusqlite, bundled): `tracks`, `playlists`, `playlist_tracks`, `lighting_cues`, `settings`.
- `dmx.rs` — `spawn_dmx_engine`, the 44 Hz Art-Net output thread (has an end-to-end UDP test).
- `programmer_ui.rs` — lighting programmer surface (fixture grid, five parameter views, effect panels).
- `show.rs` — `simulate_show()`: deterministic demo cue generator, not real authored content.
- `fader.rs` / `knob.rs` — custom egui widgets.

### Key modules — `crates/halo-light/src`

- `fixture.rs` — `Rig`, grid/selection types, `default_rig()`.
- `fixture_library.rs` — `FixtureProfile`/`Channel`/`ChannelType`, hardcoded profile registry, patching.
- `cues.rs` — `CueSet` (runtime, non-overlapping per lane) + `CueFile` (persisted JSON, seconds).
- `programmer.rs` — `resolve()`: **the single merge point** (programmer override > track cues > off).
- `output.rs` — pure `render()`: resolved lanes + params → per-universe `[u8; 512]` DMX frames.
- `artnet.rs` — synchronous Art-Net over `std::net::UdpSocket` (broadcast/unicast, multi-destination).

### Persistence

- **eframe persistence** (`PERSIST_KEY = "halo"`) — UI/mixer state: trims, keylock, pitch range, quantize,
gated mode, audition volume, device/buffer, sort order.
- **Library DB `settings` table** — Art-Net config and rig patch.
- There is no config file; `config.json` in old branches/history is from the pre-pivot console.

## Conventions

- Crate names are prefixed with `halo-` (the `halo-light` crate lives in `crates/halo-light`).
- **Never use `unsafe`** blocks or functions in any code.
- When using `format!` (and friends) and a variable can be inlined into `{}`, always inline it.
- **No tokio** — concurrency is plain threads + `mpsc` + atomics. Don't reintroduce async scaffolding.
- The audio callback must never allocate, lock, or panic.
- `programmer::resolve()` stays the single merge point for lighting state; `output::render()` stays pure.
- After changing Rust code, always run `cargo +nightly fmt --all`.

## CI

`.github/workflows/rust.yml` runs on macOS arm64 and Linux x86_64: checks out `timestretch-rs` as a sibling,
then `cargo +nightly fmt --check`, `cargo build`, `cargo test` on stable 1.90.0.
25 changes: 15 additions & 10 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,18 @@
## ⏩ Quick‑Start

### 1. Fork, clone & branch

Halo depends on the [`timestretch`](https://github.com/robmorgan/timestretch-rs) crate by local path, so clone
`timestretch-rs` alongside your halo clone:

```bash
git clone https://github.com/<your-username>/halo.git
git clone https://github.com/robmorgan/timestretch-rs.git
cd halo
git checkout -b feat/<topic>
```

### 2. Compile fast (uses stable toolchain from rust-toolchain.toml)
### 2. Compile fast (uses stable toolchain)
```bash
cargo check --workspace --all-targets
```
Expand Down Expand Up @@ -49,21 +54,21 @@ git push origin feat/<topic>

## Project Layout

| Crate | Role |
| ------------------- | -------------------------------------------------------- |
| **`halo-core`** | Core lighting engine. |
| **`halo-fixtures`** | Fixture library and management. |
| **`halo`** | CLI and main entrypoint |
| **`halo-ui`** | UI Components and Interface. |
| Crate | Role |
| ----------------- | ----------------------------------------------------------------------------------------- |
| **`halo`** | The application: UI, decks, audio engine, mixer DSP, track library, DMX engine thread. |
| **`halo-light`** | UI-free lighting domain library: fixtures, cues, programmer resolution, Art-Net output. |

All crates live in one Cargo **workspace**, so `cargo <cmd>` from the repo root affects everything.
All crates live in one Cargo **workspace**, so `cargo <cmd>` from the repo root affects everything. The
[`timestretch`](https://github.com/robmorgan/timestretch-rs) engine is a path dependency expected at
`../timestretch-rs`, next to this repository.

-----

## Dev Environment

* **Platform**: Development and execution require **macOS**.
* **Rust (Build/Test)**: **Stable** toolchain, MSRV pinned in `rust-toolchain.toml` (currently *1.90.0*). Install via [rustup.rs][rustup.rs]. This is used by default for `cargo build`, `cargo check`, `cargo test`, etc.
* **Rust (Build/Test)**: **Stable** toolchain (CI runs *1.90.0*). Install via [rustup.rs][rustup.rs]. This is used by default for `cargo build`, `cargo check`, `cargo test`, etc.
* **Rust (Format)**: **Nightly** toolchain is required *only* for formatting (`cargo fmt`) due to unstable options used in our `rustfmt.toml` configuration.
* Install via: `rustup toolchain install nightly`
* **Rust Components**: `rustfmt`, `clippy` – install via `rustup component add rustfmt clippy`. Make sure these components are available for *both* your default stable toolchain and the nightly toolchain.
Expand Down Expand Up @@ -107,7 +112,7 @@ All crates live in one Cargo **workspace**, so `cargo <cmd>` from the repo root

1. Sync with `main`; rebase preferred.
2. Ensure your code is formatted correctly with `cargo +nightly fmt --all`.
3. Ensure CI is green (build, fmt check, clippy, tests on macOS using appropriate toolchains).
3. Ensure CI is green (fmt check, build, and tests on macOS arm64 and Linux x86_64).
4. Fill out the PR template; explain *why* + *how*.
5. Respond to review comments promptly – we’re friendly, promise!
6. Maintainers will *Squash & Merge* (unless history is already clean).
Expand Down
Loading
Loading