Skip to content
Open
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
13 changes: 13 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
node_modules
.git
.env*
!.env.production
!.env.local.template
dist
build
.output
target
*.log
.turbo
coverage
.nyc_output
31 changes: 31 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# syntax=docker/dockerfile:1

FROM oven/bun:1 AS base
WORKDIR /app

# Build
FROM base AS builder
COPY package.json bun.lock* ./
RUN bun install --frozen-lockfile
COPY . .
RUN bun run build

# TODO: Switch back to Bun runtime once module resolution is fixed
# Bun doesn't properly resolve externalized Nitro packages (srvx, react-dom/server)
# Error: Cannot find package 'srvx' from '/app/.output/server/chunks/virtual/entry.mjs'
FROM node:22-slim AS runner
WORKDIR /app
ENV NODE_ENV=production

# Nitro bundles most deps but externalizes some (react-dom/server, srvx).
# Copy both .output and node_modules to ensure all SSR deps are available.
COPY --from=builder /app/.output ./.output
COPY --from=builder /app/node_modules ./node_modules
# Nitro's bundled node_modules may have Bun-specific entries missing Node variants.
# Replace them with the full copies from the top-level node_modules.
RUN rm -rf .output/server/node_modules/react-dom .output/server/node_modules/react \
&& cp -r node_modules/react-dom .output/server/node_modules/react-dom \
&& cp -r node_modules/react .output/server/node_modules/react

EXPOSE 3000
CMD ["node", ".output/server/index.mjs"]
4 changes: 2 additions & 2 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 20 additions & 1 deletion content/docs/community/contributing/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ fix(api): handle null response
documentation(readme): update install steps
```

Write **atomic commits**: each commit should represent a single logical change.
Write **atomic commits**: each commit should represent a single logical change. Do not use `!` in commit type prefixes (e.g., `feat!:`). Breaking changes are documented in PR descriptions and changesets.

### Pull Requests

Expand All @@ -53,6 +53,25 @@ All contributions go through code review. Reviewers will check for:
- Alignment with [quality pillars](/community/contributing/pillars)
- Test coverage and documentation

### AI-Assisted Development

Omni maintains shared conventions for AI coding tools in [golden/AGENTS.md](https://github.com/omnidotdev/golden/blob/master/AGENTS.md). These cover Git, TypeScript, Rust, Drizzle, infrastructure, and more.

#### Setup

1. Clone the [golden](https://github.com/omnidotdev/golden) repo
2. Configure your AI tool to load the rules:

**Claude Code**: Add this line to your `~/.claude/CLAUDE.md` (create the file if it doesn't exist):

```markdown
@/path/to/golden/CLAUDE.md
```

Replace the path with wherever you cloned golden. The rules load automatically at the start of every session.

**Other tools**: Point your AI tool's system prompt or rules file at `AGENTS.md` in the golden repo.

## What's Next?

- [Changesets](/community/contributing/changesets): *How do I version my changes?*
Expand Down
105 changes: 105 additions & 0 deletions content/docs/fabric/resonance/audio-engine.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
---
title: Audio Engine
description: Rust-native audio processing with shared-state threading
---

The Resonance audio engine is written in Rust and built around a pull-based audio graph. It prioritizes deterministic, glitch-free playback with sample-accurate timing.

## Three-Layer Architecture

The engine is organized into two threads:

```
┌──────────────────────────────────────────────────┐
│ Audio Callback (real-time, highest priority) │
│ Pulls samples through the audio graph │
│ Shares AudioGraph/Transport/Metronome via │
│ Arc<Mutex<_>> with the control thread │
└───────────────────────┬──────────────────────────┘
│ Arc<Mutex<_>>
┌───────────────────────┴──────────────────────────┐
│ Control Thread (near-real-time) │
│ Transport state, parameter changes, MIDI routing │
│ Bridges UI events to audio-safe commands │
└──────────────────────────────────────────────────┘
```

### Audio Callback

The audio callback runs at the OS audio callback priority. It processes one buffer at a time (typically 128 or 256 samples) by walking the audio graph from outputs back to inputs. The callback acquires a shared `Arc<Mutex<_>>` to access the `AudioGraph`, `Transport`, and `Metronome`. If the lock is contended (e.g., the control thread is updating state), the callback outputs silence for that buffer to avoid blocking. Lock-free communication via ring buffers is planned for a future release to reduce latency.

### Control Thread

The control thread receives commands from the UI (play, stop, seek, parameter change, track arm) and translates them into audio-safe messages. It also handles MIDI routing, automation playback, and transport state management. Commands are batched per audio buffer to minimize cross-thread traffic.

### I/O Thread (Planned)

A separate I/O thread for disk streaming, audio file decode/encode, and project save/load is planned but not yet implemented. Currently, file I/O is handled on the control thread.

## Audio Graph

The audio graph is a directed acyclic graph (DAG) of processor nodes. Each node has typed input and output ports. The graph is evaluated by pulling from the master output, which recursively pulls from upstream nodes.

```
[Audio File Reader] ──► [Gain] ──► [EQ] ──► [Bus Input]
[MIDI Source] ──► [Synth Plugin] ──► [Reverb] ──► [Bus Input]
[Bus Sum] ──► [Master]
```

Node types include:

| Node | Purpose |
|---|---|
| `AudioFileReader` | Streams samples from disk |
| `AudioInput` | Captures from hardware input |
| `PluginProcessor` | Wraps a CLAP plugin instance |
| `BuiltinEffect` | Gain, EQ, compressor, reverb, delay |
| `MidiSynth` | Routes MIDI events to instrument plugins |
| `BusSum` | Mixes multiple inputs together |
| `MasterOutput` | Final output to hardware or bounce |

Graph modifications (adding/removing nodes, changing connections) are performed on the control thread and swapped into the audio thread atomically using a triple-buffer scheme.

## Key Data Structures

### TransportState

Tracks playback position, tempo, time signature, loop region, and record state. Shared between the control and audio threads via an atomic snapshot.

```rust
struct TransportState {
playing: bool,
recording: bool,
position_samples: u64,
tempo_bpm: f64,
time_signature: (u32, u32),
loop_enabled: bool,
loop_start: u64,
loop_end: u64,
sample_rate: u32,
}
```

### MixerState

Per-channel state: volume, pan, mute, solo, and the ordered list of plugin slots.

### AutomationLane

A sorted list of breakpoints with interpolation mode (linear or bezier). The control thread evaluates lanes each buffer and sends parameter updates to the audio thread.

### MidiRouter

Maps MIDI input ports and channels to instrument plugin nodes. Supports channel filtering, transposition, and velocity curves.

## Sample-Accurate Timing

All events (note on/off, parameter changes, transport jumps) are tagged with a sample offset within the current buffer. The audio thread applies events at the exact sample position, not at buffer boundaries. This ensures timing precision of 1/sample_rate seconds (approximately 23 microseconds at 44.1 kHz).

## Thread Communication

Currently, the audio callback and control thread share state (`AudioGraph`, `Transport`, `Metronome`) via `Arc<Mutex<_>>`. If the mutex is contended when the audio callback tries to acquire it, the callback outputs silence for that buffer rather than blocking.

Lock-free threading via ring buffers is planned for a future release to reduce latency. The intended design uses SPSC ring buffers for parameter changes and transport commands, and triple-buffering for graph topology swaps.
86 changes: 86 additions & 0 deletions content/docs/fabric/resonance/automation.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
---
title: Automation
description: Breakpoint envelopes with sample-accurate parameter control
---

Automation allows any parameter in the signal chain to change over time. Volume fades, filter sweeps, effect wet/dry, tempo changes, and plugin parameters can all be automated.

## Automation Lanes

Each track can have multiple automation lanes, one per parameter. Lanes appear below the track in the arrangement view.

To add an automation lane:

1. Click the automation disclosure triangle on a track header
2. Click **Add Lane**
3. Select the parameter to automate (volume, pan, or any plugin parameter)

## Breakpoint Envelopes

Automation data is stored as a series of breakpoints (time-value pairs) with an interpolation mode between each pair.

```toml
[[automation]]
target = "volume"
points = [
{ time = 0, value = -6.0, curve = "linear" },
{ time = 96000, value = 0.0, curve = "linear" },
{ time = 480000, value = 0.0, curve = "bezier" },
{ time = 528000, value = -96.0, curve = "bezier" },
]
```

### Interpolation Modes

| Mode | Behavior |
|---|---|
| **Linear** | Straight line between points |
| **Bezier** | Smooth curve with adjustable curvature (drag the curve handle) |
| **Step** | Instant jump at the second point (hold previous value until the transition) |

## Drawing and Editing

### Drawing

1. Select the **Pencil** tool (or hold **B**)
2. Click and drag in an automation lane to draw a freehand envelope
3. Breakpoints are placed at the grid resolution (adjustable via snap settings)

### Editing

1. Select the **Pointer** tool
2. Click a breakpoint to select it, drag to move
3. Drag the curve between two points to adjust bezier curvature
4. Double-click to add a new breakpoint
5. Select and press **Delete** to remove breakpoints
6. Box-select multiple points and drag to move them together

### Range Operations

- **Copy/Paste**: Select a range of automation, copy, and paste at another position
- **Scale**: Select points and drag the top or bottom edge to scale values proportionally
- **Thin**: Reduce point density while preserving the shape (useful after freehand drawing)

## Automatable Parameters

Any parameter on any node in the audio graph can be automated:

- **Track controls**: Volume, pan, mute
- **Plugin parameters**: Every exposed parameter from CLAP plugins
- **Built-in effects**: All parameters on gain, EQ, compressor, reverb, delay
- **Sends**: Send level per bus
- **Transport**: Tempo (for tempo automation)

The parameter browser shows all available targets in a tree structure grouped by track and plugin.

## Sample-Accurate Automation

Automation values are evaluated per sample, not per buffer. The control thread pre-computes automation values for each buffer and sends them to the audio thread as a list of (sample_offset, value) pairs. The audio thread applies parameter changes at the exact sample position.

This means:

- A volume fade is perfectly smooth, with no stepping artifacts
- A filter cutoff sweep is continuous, not quantized to buffer boundaries
- Multiple parameters can change simultaneously at different rates

For parameters that plugins process per-buffer (not per-sample), the audio thread sends the parameter value at the start of each processing block. CLAP plugins that support per-sample parameter events receive the full event list.
103 changes: 103 additions & 0 deletions content/docs/fabric/resonance/browser-version.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
---
title: Browser Version
description: WASM-compiled audio engine with AudioWorklet bridge and offline support
---

> **Planned**: The browser version is not yet implemented. This page describes the intended design.

The browser version of Resonance will run at [resonance.omni.dev](https://resonance.omni.dev). It will provide a subset of the desktop functionality, compiled to WebAssembly and running in an AudioWorklet.

## What's Included

- Audio graph engine (WASM-compiled `resonance-core`)
- All built-in effects (EQ, compressor, reverb, delay)
- MIDI input via WebMIDI
- Automation lanes with full editing
- Piano roll and arrangement views
- WASM-compiled CLAP plugins (planned)
- Project export (WAV, FLAC currently; MP3, OGG planned)
- Offline support via service worker

## What's Excluded

- **Native CLAP/LV2 plugins**: No access to local plugin binaries from the browser
- **Low-latency audio**: Browser audio typically has 10-50ms latency (vs 3-10ms on desktop)
- **Filesystem access**: Projects are stored in IndexedDB, not on disk
- **Hardware multi-channel I/O**: Browser audio is typically stereo only
- **ASIO/JACK support**: No direct audio driver access

## WASM Compilation

The `resonance-web` crate compiles a subset of `resonance-core` to `wasm32-unknown-unknown`. The compilation excludes:

- Plugin host code (replaced by WASM plugin host)
- Native audio I/O (replaced by AudioWorklet bridge)
- File I/O (replaced by IndexedDB adapter)
- Thread spawning (single-threaded in WASM)

The resulting `.wasm` binary is approximately 2 MB (gzipped) and loads in under 2 seconds on a modern connection.

## AudioWorklet Bridge

The audio engine runs inside an `AudioWorkletProcessor`:

```
┌─────────────────────────────────────────────┐
│ Main Thread │
│ UI (React), project state, MIDI handling │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ SharedArrayBuffer / MessagePort │ │
│ └──────────────────┬──────────────────┘ │
└─────────────────────┼───────────────────────┘
┌─────────────────────┼───────────────────────┐
│ AudioWorklet Thread │
│ ┌──────────────────┴──────────────────┐ │
│ │ resonance-core (WASM) │ │
│ │ Audio graph evaluation │ │
│ │ Built-in DSP │ │
│ │ WASM plugin instances │ │
│ └─────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
```

Communication between the main thread and the AudioWorklet uses:

- **SharedArrayBuffer**: For audio data transfer (when available, with COOP/COEP headers)
- **MessagePort**: For commands and parameter changes (fallback if SAB unavailable)

The AudioWorklet processes 128 samples per callback (the Web Audio API default). The WASM engine is called once per callback to fill the output buffer.

## CLAP-in-WASM

CLAP plugins that are compiled to WASM can run in the browser. The workflow:

1. Plugin developer compiles their CLAP plugin to `wasm32-wasi`
2. The `.wasm` bundle is uploaded to the Resonance plugin registry
3. Users browse and install WASM plugins from within the browser version
4. The plugin runs inside the AudioWorklet alongside the engine

WASM plugins share the same AudioWorklet thread. CPU-heavy plugins may cause audio dropouts since there is no real-time thread priority in the browser.

## Offline Support

The browser version works offline after the initial load:

- **Service Worker**: Caches the app shell, WASM binary, and static assets
- **IndexedDB**: Stores projects, audio files, and plugin presets locally
- **Background Sync**: Queues cloud sync operations for when connectivity returns

The app registers a service worker on first visit. Subsequent visits load from cache, with updates applied in the background.

## Use Cases

The browser version is best suited for:

- **Sketching**: Quickly capture an idea without launching the desktop app
- **Review**: Open a shared project to listen and leave feedback
- **Collaboration**: Join a live session from any device
- **Education**: Learn music production without installing software
- **Mobile**: Basic editing on tablets (limited by screen size and touch input)

For production work (recording, mixing, mastering), the desktop app is recommended.
Loading
Loading