Skip to content

Commit 0b9b180

Browse files
authored
feat(nam): folder management — editable dir, rescan, models-folder on stage card (#245)
1 parent 7bb69f4 commit 0b9b180

16 files changed

Lines changed: 543 additions & 17 deletions

File tree

CLAUDE.md

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
Rustortion is a real-time guitar/bass amp simulator built in Rust. It runs as a standalone JACK app and as a VST3/CLAP plugin. The GUI is shared between both targets via the `rustortion-ui` crate.
8+
9+
## Workspace Crates
10+
11+
- **`rustortion-core`** — DSP engine, amp stages, IR cabinet, preset management. No GUI dependencies.
12+
- **`rustortion-ui`** — Shared GUI: stages, components, messages, handlers, i18n, `SharedApp<ParamBackend>`. Uses `iced = "0.14"`.
13+
- **`rustortion-standalone`** — Standalone JACK app. Thin shell wrapping `SharedApp<StandaloneBackend>` with MIDI, tuner, settings, recording.
14+
- **`rustortion-plugin`** — VST3/CLAP plugin via nih-plug. Editor uses `iced_baseview` + `SharedApp<PluginBackend>`.
15+
- **`xtask`** — Build automation.
16+
17+
## Build & Development Commands
18+
19+
```bash
20+
# Build and run standalone (requires JACK/PipeWire)
21+
cargo run --release
22+
23+
# Build plugin
24+
cargo build -p rustortion-plugin --release
25+
26+
# Lint (formatting + clippy) — this is what CI runs
27+
make lint
28+
29+
# Run tests
30+
make test # all tests
31+
cargo test test_name # single test
32+
33+
# Benchmarks
34+
make bench
35+
36+
# Coverage (requires cargo-tarpaulin)
37+
make cover
38+
```
39+
40+
**System dependencies** (must be installed before building):
41+
```bash
42+
sudo apt-get install libjack-jackd2-dev libasound2-dev pkg-config
43+
```
44+
45+
**Clippy flags** used in CI: `-D warnings -D clippy::all -D clippy::pedantic -D clippy::nursery`
46+
(`lib.rs` has `#![allow(...)]` overrides for specific pedantic/nursery lints)
47+
48+
**Dev profile** uses `opt-level = 1` because IR cabinet processing is too slow in pure debug mode.
49+
50+
## Architecture
51+
52+
### Shared GUI Pattern
53+
54+
Both standalone and plugin use `SharedApp<B: ParamBackend>` from `rustortion-ui`:
55+
56+
```
57+
rustortion-standalone rustortion-plugin
58+
AmplifierApp PluginApp (iced_baseview::Application)
59+
└─ SharedApp<StandaloneBackend> └─ SharedApp<PluginBackend>
60+
└─ StandaloneBackend └─ PluginBackend
61+
└─ Manager/Engine (JACK) └─ EngineHandle + GuiContext
62+
```
63+
64+
`ParamBackend` trait (`rustortion-ui/src/backend.rs`) abstracts engine communication. `Capabilities` struct controls which UI sections render (e.g. plugin hides tuner, MIDI config, recording, settings).
65+
66+
### Audio Signal Flow
67+
68+
```
69+
Input → [Tuner bypass] → Input Filters (HP/LP) → [Upsample] → Amp Chain (stages) → [Downsample] → Pitch Shifter → IR Cabinet → Peak Meter → Recorder → Output
70+
```
71+
72+
### Key Modules
73+
74+
#### rustortion-core
75+
- **`src/amp/chain.rs`** — Ordered list of processing stages.
76+
- **`src/amp/stages/`** — 10 registered DSP stages: preamp, compressor, noise_gate, tonestack, poweramp, multiband_saturator, level, delay, reverb, eq. Plus utilities: `clipper`, `filter`, `common`.
77+
- **`src/audio/engine.rs`** — Core audio processing loop. Controlled via crossbeam channels.
78+
- **`src/ir/`** — IR cabinet, convolver (FIR/FFT), loader.
79+
- **`src/preset/`** — Preset save/load/delete, `StageConfig` enum, `InputFilterConfig`.
80+
81+
#### rustortion-ui
82+
- **`src/app.rs`**`SharedApp<B>` — shared state, update(), view(), subscription().
83+
- **`src/backend.rs`**`ParamBackend` trait, `Capabilities`, `ExternalEvent`.
84+
- **`src/stages/mod.rs`**`gui_stage_registry!` macro, `ParamUpdate`, all 10 stage view modules.
85+
- **`src/components/`** — Reusable UI components: widgets, dialogs, preset_bar, peak_meter, ir_cabinet_control, minimap, etc.
86+
- **`src/handlers/`** — Portable handlers: preset, hotkey.
87+
- **`src/messages/`** — Message enums for Iced event-driven updates.
88+
- **`src/i18n/`**`tr!()` macro, EN + ZH_CN locales.
89+
- **`src/tabs.rs`** — Tab navigation: Amp, Effects, Cabinet, IO.
90+
91+
#### rustortion-standalone
92+
- **`src/gui/app.rs`**`AmplifierApp` wrapping `SharedApp<StandaloneBackend>` + standalone handlers (MIDI, tuner, settings, recording).
93+
- **`src/backend.rs`**`StandaloneBackend` implementing `ParamBackend` via `Manager`/`Engine`.
94+
- **`src/audio/`** — JACK client, Manager, ports.
95+
- **`src/gui/handlers/`** — Standalone-only: midi, tuner, settings.
96+
- **`src/gui/components/dialogs/`** — Standalone-only dialogs: midi, settings, tuner.
97+
98+
#### rustortion-plugin
99+
- **`src/lib.rs`** — nih-plug `Plugin` impl, audio processing, initialization.
100+
- **`src/editor.rs`**`PluginEditor` (nih-plug `Editor` trait) + `PluginApp` (iced_baseview `Application`).
101+
- **`src/backend.rs`**`PluginBackend` implementing `ParamBackend` via `EngineHandle` + `GuiContext`.
102+
- **`src/params.rs`** — Full nih-plug parameter set: global params + 8 slots × 10 stage types.
103+
104+
### Stage Registration (`rustortion-ui/src/stages/mod.rs`)
105+
106+
The `gui_stage_registry!` macro generates `StageType`, `StageConfig`, and `StageMessage` enums plus all boilerplate. Adding a new stage requires:
107+
1. Add one line to the macro invocation
108+
2. Create `rustortion-ui/src/stages/new_stage.rs` with config, message, and view implementations
109+
3. Create `rustortion-core/src/amp/stages/new_stage.rs` implementing the `Stage` trait
110+
4. Add i18n keys to EN and ZH_CN in `rustortion-ui/src/i18n/mod.rs`
111+
5. Add slot params to `rustortion-plugin/src/params.rs`
112+
113+
### Thread Model
114+
115+
The JACK process callback (standalone) or nih-plug `process()` (plugin) runs on a real-time thread. The GUI communicates with the engine via crossbeam channels. Shared state (tuner data, peak meter) uses `ArcSwap` for lock-free reads.
116+
117+
## Common Pitfalls
118+
119+
- **JACK/PipeWire must be running** before `cargo run --release`. If JACK is not available the app will panic on startup.
120+
- **Dev profile uses `opt-level = 1`** — benchmarks and performance comparisons must use `--release`.
121+
- **The `gui_stage_registry!` macro** in `rustortion-ui/src/stages/mod.rs` generates boilerplate. Do not hand-write — add one line to the macro invocation instead.
122+
- **Preset JSON format** — each preset is a JSON file in `~/.config/rustortion/presets/`. Structure: `{ "name": "...", "stages": [...], "ir_name": "...", "ir_gain": N, "pitch_shift_semitones": N, "input_filters": {...} }`.
123+
- **IR files** are in `impulse_responses/` and `~/.config/rustortion/impulse_responses/`. Loading is async (off RT thread).
124+
- **Clippy is strict** — CI runs `-D warnings -D clippy::all -D clippy::pedantic -D clippy::nursery`.
125+
- **iced_baseview** is a fork at `github.com/OpenSauce/iced_baseview`, upgraded to iced 0.14 crates.io.
126+
127+
## Conventions
128+
129+
- Rust edition 2024
130+
- Conventional commits: `feat:`, `fix:`, `refactor:`, `chore:`, etc.
131+
- Changelog generated via `git-cliff`
132+
- Standalone entry point: `rustortion-standalone/src/bin/gui.rs`
133+
- Releases via `cargo-dist` (`.github/workflows/release.yml`, `dist-workspace.toml`)

presets/Clean_Eb.json

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
{
2+
"name": "Clean Eb",
3+
"description": null,
4+
"author": null,
5+
"stages": [
6+
{
7+
"Compressor": {
8+
"attack_ms": 1.0,
9+
"release_ms": 100.0,
10+
"threshold_db": -20.0,
11+
"ratio": 4.0,
12+
"makeup_db": 3.9000003
13+
}
14+
},
15+
{
16+
"Preamp": {
17+
"gain": 1.1,
18+
"bias": 1.4901161e-8,
19+
"clipper_type": "Triode"
20+
}
21+
},
22+
{
23+
"ToneStack": {
24+
"model": "British",
25+
"bass": 1.0500001,
26+
"mid": 1.4,
27+
"treble": 1.25,
28+
"presence": 1.45
29+
}
30+
},
31+
{
32+
"MultibandSaturator": {
33+
"low_drive": 0.099999994,
34+
"mid_drive": 0.17999999,
35+
"high_drive": 0.11,
36+
"low_level": 1.0,
37+
"mid_level": 1.54,
38+
"high_level": 1.0,
39+
"low_freq": 325.0,
40+
"high_freq": 2850.0
41+
}
42+
},
43+
{
44+
"Level": {
45+
"gain": 0.90000004
46+
}
47+
},
48+
{
49+
"Eq": {
50+
"gains": [
51+
-8.6,
52+
-7.0,
53+
-3.3,
54+
0.0,
55+
0.0,
56+
0.0,
57+
0.0,
58+
2.8000002,
59+
3.6000001,
60+
1.6000001,
61+
3.1000001,
62+
3.2000003,
63+
-0.29999983,
64+
-4.9,
65+
-2.9999998,
66+
-4.2999997
67+
]
68+
}
69+
},
70+
{
71+
"Reverb": {
72+
"room_size": 0.24,
73+
"damping": 0.96,
74+
"mix": 0.06
75+
}
76+
},
77+
{
78+
"Delay": {
79+
"delay_ms": 300.0,
80+
"feedback": 0.3,
81+
"mix": 0.26
82+
}
83+
}
84+
],
85+
"ir_name": "Science Amplification/4x12/G12H-150/MD 421-U Brighter.wav",
86+
"ir_gain": 0.34,
87+
"pitch_shift_semitones": -1,
88+
"input_filters": {
89+
"hp_enabled": true,
90+
"hp_cutoff": 117.0,
91+
"lp_enabled": true,
92+
"lp_cutoff": 7174.0
93+
}
94+
}

presets/Djent_Eb.json

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
{
2+
"name": "Djent Eb",
3+
"description": null,
4+
"author": null,
5+
"stages": [
6+
{
7+
"Preamp": {
8+
"gain": 3.3,
9+
"bias": 0.0,
10+
"clipper_type": "ClassA"
11+
}
12+
},
13+
{
14+
"ToneStack": {
15+
"model": "Modern",
16+
"bass": 0.90000004,
17+
"mid": 1.2,
18+
"treble": 1.1,
19+
"presence": 0.95
20+
}
21+
},
22+
{
23+
"NoiseGate": {
24+
"threshold_db": -20.0,
25+
"ratio": 10.0,
26+
"attack_ms": 1.4,
27+
"hold_ms": 10.0,
28+
"release_ms": 100.0
29+
}
30+
},
31+
{
32+
"Compressor": {
33+
"attack_ms": 1.0,
34+
"release_ms": 80.0,
35+
"threshold_db": -14.0,
36+
"ratio": 4.0,
37+
"makeup_db": 18.5
38+
}
39+
},
40+
{
41+
"Preamp": {
42+
"gain": 6.5,
43+
"bias": 0.70000005,
44+
"clipper_type": "Triode"
45+
}
46+
},
47+
{
48+
"MultibandSaturator": {
49+
"low_drive": 0.42,
50+
"mid_drive": 0.59999996,
51+
"high_drive": 0.64,
52+
"low_level": 0.91999996,
53+
"mid_level": 0.97999996,
54+
"high_level": 0.97999996,
55+
"low_freq": 413.0,
56+
"high_freq": 2410.0
57+
}
58+
},
59+
{
60+
"Level": {
61+
"gain": 0.25
62+
}
63+
},
64+
{
65+
"Eq": {
66+
"gains": [
67+
-9.2,
68+
-8.6,
69+
-5.7,
70+
-3.4999998,
71+
-0.29999983,
72+
0.0,
73+
0.0,
74+
0.0,
75+
0.0,
76+
0.0,
77+
0.0,
78+
0.0,
79+
-0.29999983,
80+
-0.39999983,
81+
-1.9999999,
82+
-6.7
83+
]
84+
}
85+
},
86+
{
87+
"Reverb": {
88+
"room_size": 0.32999998,
89+
"damping": 0.39,
90+
"mix": 0.089999996
91+
}
92+
}
93+
],
94+
"ir_name": "Science Amplification/4x12/G12H-150/SM57 Brighter.wav",
95+
"ir_gain": 0.42,
96+
"pitch_shift_semitones": -1,
97+
"input_filters": {
98+
"hp_enabled": true,
99+
"hp_cutoff": 133.0,
100+
"lp_enabled": true,
101+
"lp_cutoff": 7469.0
102+
}
103+
}

rustortion-plugin/src/backend.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,17 @@ impl ParamBackend for PluginBackend {
245245
None
246246
}
247247

248+
fn nam_models_dir(&self) -> Option<std::path::PathBuf> {
249+
Some(crate::user_nam_dir())
250+
}
251+
252+
fn rescan_nam_models(&self) -> Result<usize, String> {
253+
let dir = crate::user_nam_dir();
254+
let loader = rustortion_core::nam::NamLoader::new(&dir).map_err(|e| e.to_string())?;
255+
rustortion_core::nam::registry::init_from_loader(&loader);
256+
Ok(loader.available_names().len())
257+
}
258+
248259
fn persist_chain_state(&self, stages: &[StageConfig]) {
249260
// Store in SharedState for editor close/reopen within same session
250261
self.shared_state.store_gui_stages(stages);

rustortion-plugin/src/lib.rs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,17 @@ pub mod params;
1313

1414
use params::RustortionParams;
1515

16+
/// Directory the plugin loads user-provided `.nam` models from:
17+
/// `~/.config/rustortion/nam`. Shared by the init-time loader and the backend's
18+
/// rescan so the two can never drift to different paths.
19+
#[must_use]
20+
pub fn user_nam_dir() -> std::path::PathBuf {
21+
dirs::config_dir()
22+
.unwrap_or_default()
23+
.join("rustortion")
24+
.join("nam")
25+
}
26+
1627
enum PluginTask {
1728
LoadPreset(String),
1829
/// Combined task: create new samplers at the given factor, then reload the
@@ -376,10 +387,7 @@ impl Plugin for RustortionPlugin {
376387

377388
// Load user NAM models from ~/.config/rustortion/nam into the
378389
// process-global registry so the NAM stage can resolve models.
379-
let nam_dir = dirs::config_dir()
380-
.unwrap_or_default()
381-
.join("rustortion")
382-
.join("nam");
390+
let nam_dir = user_nam_dir();
383391
match rustortion_core::nam::NamLoader::new(&nam_dir) {
384392
Ok(loader) => {
385393
nih_log!("Loaded {} NAM model(s)", loader.available_names().len());

0 commit comments

Comments
 (0)