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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ vite.config.ts.timestamp-*
/flatpak/.flatpak-builder/
/flatpak/splitwave.deb
/flatpak/*.flatpak

5 changes: 3 additions & 2 deletions docs/CONCEPT.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,10 @@ underscore-prefixed (`_slider.svelte`).

## Frontend

Detailed UI conventions and Svelte 5 runes architecture are documented in [UI.md](UI.md).

- Svelte 5 runes only. No `export let`, no stores in component scope.
- xyflow nodes wrap with `Wrapper` from `flow/ui/node.svelte`
(`accent`, `hasInput`, `hasOutput`).
- xyflow nodes wrap with `Wrapper` from `flow/ui/node.svelte` (`accent`, `hasInput`, `hasOutput`).
- Interactive elements inside nodes: `nodrag nopan` (+ `nowheel` if scrollable).
- Numeric readouts: `font-mono tabular-nums`.
- IDs: `@paralleldrive/cuid2`. Not nanoid, not uuid.
Expand Down
138 changes: 94 additions & 44 deletions docs/RULES.md
Original file line number Diff line number Diff line change
@@ -1,67 +1,117 @@
# RULES.md

Universal rules that apply to every change in this repo. Concept and
architecture background: [CONCEPT.md](CONCEPT.md). What a new feature must
look like: [FEATURES.md](FEATURES.md). PR hygiene:
[CONTRIBUTING.md](../CONTRIBUTING.md).
Universal rules that apply to every change in this repository.
Architecture background: [CONCEPT.md](CONCEPT.md).
Feature specifications: [FEATURES.md](FEATURES.md).
UI Architecture and Runes conventions: [UI.md](UI.md).
PR checklist and testing: [CONTRIBUTING.md](../CONTRIBUTING.md).

---

## Sections

- [Smallest viable change](#smallest-viable-change)
- [No silent fallback](#no-silent-fallback)
- [Comments](#comments)
- [Formatting](#formatting)
- [File Size and Decomposition](#file-size-and-decomposition)
- [Cross-Platform Abstraction](#cross-platform-abstraction)
- [No Silent Fallback and Error Handling](#no-silent-fallback-and-error-handling)
- [Real-Time (RT) Audio Path Invariants](#real-time-rt-audio-path-invariants)
- [Comments and Documentation](#comments-and-documentation)
- [Formatting and Linting](#formatting-and-linting)
- [Commits](#commits)
- [When in doubt](#when-in-doubt)
- [Verification Checklist](#verification-checklist)

---

## File Size and Decomposition

- **Line Budget**: Target < 400 lines per file; hard ceiling of 500 lines.
- When a file exceeds 500 lines, it must be decomposed into a dedicated module folder with a clean `mod.rs` and single-responsibility submodules.
- Avoid monolith dumping grounds (e.g. putting all IPC commands or all effects in one file). Group by domain and lifecycle.
- In Svelte components, separate complex Canvas drawing, event history, or specialized sub-controls into helper classes (`.ts`) or internal sub-components (`_prefixed.svelte`).

---

## Cross-Platform Abstraction

- Platform-specific files (`macos.rs`, `windows.rs`, `linux.rs`) must contain **only** code directly interfacing with OS-specific APIs (CoreAudio/SCK, WASAPI/SetupAPI, PipeWire/PulseAudio).
- Shared logic (e.g., CPAL device enumeration and stream building, caching layers, input validation, channel conversion, data normalization) MUST live in `mod.rs` or a shared helper.
- If identical code appears in two or more platform files, factor it into `mod.rs` immediately.
- A platform that cannot support a given capability returns a typed `AppError`; it never pretends to succeed or quietly substitutes another device or sample rate.

---

## No Silent Fallback and Error Handling

- **One deterministic path per decision**: Surface failures to the user. Never substitute a different audio device, sample rate, or channel format behind the user's back.
- **Typed Errors**: Use `AppError` variants (`Host`, `Device`, `Stream`, `Validation`, `Plugin`).
- **No Uncontrolled Panics**: Never call `.unwrap()` or `.expect()` in runtime code paths, especially inside IPC commands or audio threads.
- Error states must propagate cleanly to the UI through explicit events (`audio://input_error`, `audio://speaker_error`, `error://panic`) rather than failing silently.

## Smallest viable change
---

- Long specs are upper bounds — slice them.
- Touch only the lines the change requires.
- No drive-by renames, import reordering, or refactors of code the change
does not otherwise touch. Separate PR.
- If the same UI block or helper appears twice, factor it out before opening
the PR.
## Real-Time (RT) Audio Path Invariants

## No silent fallback
The real-time path comprises all callbacks executed by cpal, ScreenCaptureKit, CoreAudio, PipeWire, WASAPI, and the inner loop of `DspWorker::run`.

One deterministic path per decision. Surface failures — never substitute a
different device, rate, or format behind the user's back. A backend that
cannot support the feature returns an error; it does not quietly fall back.
### Forbidden inside RT Audio Path:

## Comments
- ❌ **Allocations**: Growing vectors (`Vec::push`, `Vec::resize`), strings (`String::from`), box allocations (`Box::new`), hash maps.
- ❌ **System Locks**: `Mutex::lock`, `RwLock::write`. (Only lock-free atomic swaps or non-blocking `try_lock` if dropping a block is strictly acceptable).
- ❌ **Syscalls and I/O**: File access, sockets, logging macros (`tracing::info!`, `println!`), IPC.
- ❌ **Unbounded Loops**: Catch-up loops that iterate indefinitely without yielding to the transport clock.

- Comments only for non-obvious WHY (hidden constraint, invariant, workaround).
Naming handles WHAT. Terse, one line. Section dividers only in files
> 500 lines.
- Comments describe the code as it stands, never the edit or the conversation.
No "now / instead of / previously / was", no narrating a change to the
reviewer.
### Permitted inside RT Audio Path:

## Formatting
- ✅ **Preallocated Buffers**: Slices and arrays allocated during stream initialization.
- ✅ **Lock-Free Rings**: `rtrb` SPSC ring buffers using bulk operations (`bulk_pop`, `bulk_push`).
- ✅ **Atomics**: `Arc<AtomicU32>`, `Arc<AtomicBool>` with `Ordering::Relaxed` for runtime controls and meter telemetry.
- ✅ **Deterministic DSP**: Fixed-frame mathematical transformations and inline filter evaluations.

- Enforced: `bun run format` (Prettier + rustfmt) before every PR.
- The tree is clean under both, so it produces no churn — it only normalises
the lines you wrote.
- Format-on-save is safe here and encouraged. Do not hand-format against the
tool.
---

## Comments and Documentation

- **Focus on the Non-Obvious WHY**: Comments explain hidden invariants, hardware workarounds, concurrency assumptions, and mathematical rationale. Naming handles WHAT.
- **Terse and Timeless**: Describe the code as it currently exists.
- **Forbidden Comments**:
- ❌ Never write conversational change logs ("now instead of", "was previously", "changed to fix bug", "old implementation").
- ❌ Never narrate trivial mechanics (`// return result`, `// increment counter`).
- ❌ Never leave abandoned `TODO` or `FIXME` without a tracking issue.
- **Mandatory Comments**:
- ✅ Invariants on memory ordering (`Ordering::Relaxed` vs `Ordering::SeqCst`).
- ✅ Hardware or OS quirks (e.g. CoreAudio reference cycles, Windows MTA COM initialization, PipeWire RTKit quantum semantics).
- ✅ Buffer sizing assumptions (e.g. ring buffer capacities, FFT chunk requirements).

---

## Formatting and Linting

- Run `bun run format` (Prettier for TS/Svelte, rustfmt for Rust) before every commit.
- Tree must remain 100% clean under both formatters with zero manual formatting conflicts.
- Svelte runes must satisfy `bun run check` with 0 errors.
- Rust code must satisfy `cargo check` and `cargo test` with 0 warnings/errors.

---

## Commits

Format:

```
type(scope): subject
```

[Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/).
Lowercase, no trailing period, body usually omitted. Types in use: `feat`,
`fix`, `chore`, `refactor`, `style`, `docs`. Keep formatting-only commits
separate from behavioural ones so they can be reviewed at a glance and
skipped in `git blame`.
- Standard [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/).
- Valid types: `feat`, `fix`, `refactor`, `chore`, `docs`, `test`, `style`.
- Keep formatting-only commits separate from behavioral changes to preserve clean `git blame`.

---

## Verification Checklist

## When in doubt
Before considering any refactoring complete:

- Read the current code, not earlier explanations.
- RT path change → `cargo check`.
- Svelte change → `bun run check`.
- Rust `#[derive(TS)]` change → `bun run generate`, commit the generated
files with the Rust change.
1. `cargo check --manifest-path src-tauri/Cargo.toml` passes.
2. `cargo test --manifest-path src-tauri/Cargo.toml` passes all unit and integration tests.
3. `bun run check` passes with 0 errors.
4. If Rust structs with `#[derive(TS)]` were modified, run `bun run generate` and commit the generated types.
5. Run `bun run format`.
120 changes: 120 additions & 0 deletions docs/UI.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# UI Building Guidelines

Practical reference for building graph nodes, controls, and settings screens in Splitwave. Follow this to maintain visual parity with existing components.

---

## 1. Graph Node Anatomy

Every node in the graph editor must follow this exact layout contract:

```svelte
<script lang="ts">
import { useSvelteFlow, type NodeProps } from '@xyflow/svelte';
import Wrapper from '../node.svelte';
import Slider from './_slider.svelte';
import { SomeIcon } from '$lib/components/icons';

let { id, data }: NodeProps<MyNodeType> = $props();
</script>

<Wrapper label="My Node" icon={SomeIcon} accent="effect" hasInput hasOutput channelIo nodeId={id}>
<div class="nowheel nodrag flex w-48 flex-col gap-1.5">
<Slider label="Parameter" value={data.param} min={-24} max={24} step={0.5} unit=" dB" defaultValue={0} onChange={setParam} />
</div>
</Wrapper>
```

### Width & Sizing Rules:

- **Base Width**:
- `w-48` for simple nodes (Gain, Mute, Delay).
- `w-52` for detailed nodes (Compressor, EQ).
- Uncapped width (`wide={true}`) is allowed **only** for full-width visualizers (Waveform Scope, Spectrum, Multi-channel Level Meters).
- **Vertical Rhythm**: Controls stack with `flex flex-col gap-1.5`.
- **Canvas Drag Isolation**: All interactive inputs, buttons, sliders, and steppers **must** include CSS classes `nodrag nopan`. Any scrollable sub-container must also include `nowheel`.

---

## 2. Category Color Accents

Every node is color-coded by its functional category via the `accent` prop on `Wrapper`:

| Category | `accent` Prop | Text & Icon Class | Role in Graph |
| :---------- | :------------ | :--------------------------------------- | :------------------------------------------- |
| **Input** | `"input"` | `text-emerald-600 dark:text-emerald-400` | Microphones, system audio, file player |
| **Effect** | `"effect"` | `text-violet-600 dark:text-violet-400` | EQ, compressor, gate, reverb, plugins |
| **Output** | `"output"` | `text-sky-600 dark:text-sky-400` | Speakers, headphones, file recorder |
| **Monitor** | `"monitor"` | `text-amber-600 dark:text-amber-400` | Level meter, LUFS meter, waveform, spectrum |
| **Network** | `"network"` | `text-rose-600 dark:text-rose-400` | WebRTC collaborator, network sender/receiver |

---

## 3. Reusable Control Components Catalog

Do not hand-roll custom inputs or sliders. Reuse standard primitives:

1. **`Slider` (`src/lib/modules/flow/ui/effect/_slider.svelte`)**:
- Standard control for continuous parameters (dB, ms, Hz, %).
- Always supply `defaultValue` (double-clicking the track resets to it).
- Double-clicking the numeric badge opens inline text input.
2. **`SegmentedButtons` (`src/lib/components/segmented_buttons.svelte`)**:
- Mode switcher for 2 to 4 mutually exclusive states (e.g. `Stereo | Mono`, `New | Overwrite | Append`).
3. **`NumberStepper` (`src/lib/components/number_stepper.svelte`)**:
- Stepper with `+` / `-` buttons for discrete integers (channel count, snapshot limits).
4. **`Combobox` + `RescanButton` (`src/lib/modules/form/ui/`)**:
- Dropdown with search for hardware devices, audio formats, or apps. Always place `RescanButton` adjacent when enumerating audio endpoints.
5. **`PresetBar` (`src/lib/modules/preset/ui/preset_bar.svelte`)**:
- Placed at the bottom of effect nodes for loading factory and user presets.
6. **`Tooltip` (`src/lib/modules/overlay/ui/`)**:
- Hover tooltips for abbreviations, routing indicators, and warnings.

---

## 4. Typography & Audio Readouts

- **Tabular Monospace Numbers**:
Every dB readout, frequency, latency figure, millisecond duration, or timer **must** use:
```html
<span class="font-mono text-xs tabular-nums">...</span>
```
This prevents layout jitter as numbers fluctuate in real time.
- **Value Formatting**:
Use format helpers from `src/lib/components/format.ts`:
- `formatHz(48000)` -> `"48 kHz"`
- `formatDuration(sec)` -> `"00:15"`
- `formatSize(bytes)` -> `"12.4 MB"`

---

## 5. Visualizers & Curves

When displaying audio graphs, transfer curves, or level meters:

- Place the visualizer inside a recessed well for contrast:
`rounded-lg border border-neutral-400/50 bg-neutral-100/60 p-1.5`.
- SVG curve dimensions should be fixed (e.g. `130x60` for compressor transfer curves).
- Line colors on SVG:
- Grid / axes: `stroke-neutral-400/40`
- Active response curve: category accent color (e.g. `stroke-violet-600 dark:stroke-violet-400`).

---

## 6. Settings Pages & Dialogs

For standalone views outside the graph editor (`settings/+page.svelte`, `virtual-devices/+page.svelte`):

- **Section Layout**:
```html
<section class="flex flex-col gap-3">
<div>
<h2 class="text-sm font-semibold text-theme">Section Title</h2>
<p class="text-xs text-neutral-900">Brief explanation of setting.</p>
</div>
<!-- Options grid -->
</section>
```
- **Option Selection Cards (Grid buttons)**:
- Grid: `grid grid-cols-2 gap-2` or `grid-cols-3 gap-2`.
- Active state: `border-neutral-900 bg-neutral-200 text-theme`.
- Inactive state: `border-neutral-400 bg-neutral-100 text-neutral-1000 hover:bg-neutral-200`.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"tauri": "tauri",
"format": "prettier --write \"**/*.{ts,tsx,md,svelte,json}\" && cargo fmt --manifest-path src-tauri/Cargo.toml",
"generate": "cd src-tauri && cargo test"
"generate": "cd src-tauri && cargo test -- --test-threads=1",
"test": "cargo test --manifest-path src-tauri/Cargo.toml -- --test-threads=1"
},
"license": "MIT",
"dependencies": {
Expand Down
29 changes: 4 additions & 25 deletions src-tauri/src/audio/device/macos.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
use std::collections::HashSet;

use cpal::traits::{DeviceTrait, HostTrait};

use crate::audio::macos_hal;
Expand All @@ -25,38 +23,19 @@ pub fn device_info(kind: DeviceKind, name: &str) -> AppResult<NativeDeviceInfo>
}

pub fn list_inputs() -> AppResult<Vec<DeviceInfo>> {
Ok(unique_named(
macos_hal::list_input_devices()
.into_iter()
.map(|d| d.name)
.collect(),
Ok(super::unique_named(
macos_hal::list_input_devices().into_iter().map(|d| d.name),
DeviceKind::Input,
))
}

pub fn list_outputs() -> AppResult<Vec<DeviceInfo>> {
Ok(unique_named(
macos_hal::list_output_devices()
.into_iter()
.map(|d| d.name)
.collect(),
Ok(super::unique_named(
macos_hal::list_output_devices().into_iter().map(|d| d.name),
DeviceKind::Output,
))
}

fn unique_named(names: Vec<String>, kind: DeviceKind) -> Vec<DeviceInfo> {
let mut seen = HashSet::new();
names
.into_iter()
.filter(|n| seen.insert(n.clone()))
.map(|name| DeviceInfo {
id: name.clone(),
name,
kind,
})
.collect()
}

// `host.devices()` returns is_default=false -> cpal uses HalOutput bound to a
// specific AudioDeviceID. `default_*_device` returns is_default=true -> cpal
// uses DefaultOutput which silently follows the system default when it changes.
Expand Down
16 changes: 16 additions & 0 deletions src-tauri/src/audio/device/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,22 @@ pub struct NativeDeviceInfo {
pub sample_format: &'static str,
}

pub(crate) fn unique_named<I>(names: I, kind: DeviceKind) -> Vec<DeviceInfo>
where
I: IntoIterator<Item = String>,
{
let mut seen = std::collections::HashSet::new();
names
.into_iter()
.filter(|n| seen.insert(n.clone()))
.map(|name| DeviceInfo {
id: name.clone(),
name,
kind,
})
.collect()
}

#[cfg(target_os = "macos")]
mod macos;
#[cfg(target_os = "macos")]
Expand Down
Loading
Loading