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
30 changes: 28 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,36 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.2.0] - 2026-07-16

Deep-dive voicing pass (research-derived, sourced from manuals/forums/physics standards - not measured hardware, see `docs/design-brief.md`'s Honesty section) plus the suite's first M2 preset system implementation (pilot for the other 11 plugins), a German frame-string localisation, and an app icon. Also folds in a run of fixes/housekeeping commits merged after v0.1.0 that never got their own release.

### Changed

- **Distance's low-shelf taper is now front-loaded, not linear** (`docs/design-brief.md`'s "Distance" module spec): real proximity effect concentrates most of its audible change early in a mic's travel and saturates, rather than growing evenly - `CabConvolutionEngine`'s low-shelf cut now scales through an "ease-out" power curve (`1 - (1 - normalisedDistance)^1.8`) instead of plain-linear-in-dB. The high-shelf keeps its linear taper (a deliberate asymmetry - see `docs/architecture.md`'s "v0.2.0 Distance taper" note). **This is parameter-behavior-breaking pre-1.0**: a session with Distance at, say, 60% will sound different after this update, even though the parameter's own range/default are unchanged and no migration is needed (see the design brief's Versioning section).
- **Doc/manual wording**: replaced "air-absorption darkening" framing for Distance's high-shelf with a directivity-first explanation (`docs/manual.md`, `docs/architecture.md`, `README.md`) - the audible effect is real, but at typical reamping distances it's driven far more by loudspeaker directivity than literal atmospheric absorption.
- **LoCut/HiCut ranges (20-800 Hz / 2-20 kHz) are now explicitly documented as a deliberate keep**, not an unexamined default, after comparison against the closest structural reference plugin (Ignite Amps NadIR: 10-400 Hz / 6-22 kHz) - see `docs/architecture.md` and the new named regression test in `tests/EngineTests.cpp`. No range or default values changed.

### Added

- **M2 preset system** (`.scaffold/specs/preset-system-m2.md`, binding suite-wide spec - Nave is the pilot implementation): `src/presets/PresetManager` (factory presets embedded via BinaryData, user presets on disk, load/save/rename/delete, dirty-state tracking, prev/next navigation, single-file and zip-bank import/export, user-preset-wins-over-factory default resolution) and `src/presets/PresetBar` (the editor strip, docked at the top of the existing v0.1/v0.2 layout). Written with no Nave-specific coupling beyond a small config struct, documented as a sibling-plugin replication recipe in `docs/preset-system-notes.md`.
- **8 factory presets** (`presets/factory/*.json`, documented in `docs/presets.md`): Default, Tame the Fizz, Live Stage, Dark Vintage, Pushed Back in the Room, Touch of Room Mic, Even Blend, Parallel Cab (Blended Dry) - sourced starting points from `docs/design-brief.md`, tunable/auditionable, not claimed as hardware-matched.
- **German frame-string localisation**: `resources/i18n/de.txt`, selected automatically via `SystemStats::getUserLanguage()` for PresetBar's labels/menus/dialogs. Parameter/DSP terminology (LoCut, HiCut, Distance, Mix, Level, Hz, dB, %) is never translated, in this pass or any future one.
- **App icon wired into the plugin binaries** (`ICON_BIG` in `CMakeLists.txt`, pointing at the icon asset added in v0.1.0's branding pass) - previously only used in docs/README, not embedded in the built AU/VST3/Standalone.
- `docs/design-brief.md` and `docs/research-notes.md`: the sourced design brief and research notes this release's voicing/doc changes are derived from.
- Manual now notes that loading two different real-world IRs can land at different output levels because `Convolution::Normalise::yes` is an energy normalisation, not a perceptual loudness match - pointing at Level as the fix (`docs/manual.md`).
- New Catch2 coverage: Distance taper front-loading/monotonicity test, a fixed-point taper regression snapshot, a named LoCut/HiCut range-guard test, and 16 preset-system tests (`tests/PresetManagerTests.cpp` - round-trip, forward-tolerant import, wrong-plugin/wrong-format refusal, factory preset integrity, default resolution order, dirty-flag lifecycle, prev/next wrap-around, rename/delete guards, single-file and bank import/export).

### Fixed

- **`convolutionB` not reset on Blend's disengaged->engaged transition** (#12): unlike LoCut/HiCut/Distance, IR B's convolution engine kept no history of its own bypass state, so its internal overlap-add tail could go stale (frozen, not decaying) while Blend was disengaged and then leak into the output the moment Blend re-engaged. `CabConvolutionEngine` now tracks `blendEngagedPreviously` and calls `convolutionB.reset()` on the same disengaged->engaged transition the other stages already handle.
- **Reloading IR A after IR B silently invalidated IR B's phase alignment** (#13): `setImpulseResponse()`/`loadDefaultImpulseResponse()` recorded IR A's new onset as the phase-alignment reference but never re-ran IR B's alignment against it, leaving an already-loaded IR B aligned to a stale, overwritten onset (reintroducing comb-filtering on the next Blend crossfade). `CabConvolutionEngine` now retains a copy of IR B's raw, pre-alignment buffer and automatically re-aligns it whenever IR A's reference onset changes.
Carried over from commits merged after the v0.1.0 tag that never shipped in a release:

- **`convolutionB` not reset on Blend's disengaged->engaged transition** (#12, PR #18): unlike LoCut/HiCut/Distance, IR B's convolution engine kept no history of its own bypass state, so its internal overlap-add tail could go stale (frozen, not decaying) while Blend was disengaged and then leak into the output the moment Blend re-engaged. `CabConvolutionEngine` now tracks `blendEngagedPreviously` and calls `convolutionB.reset()` on the same disengaged->engaged transition the other stages already handle.
- **Reloading IR A after IR B silently invalidated IR B's phase alignment** (#13, PR #18): `setImpulseResponse()`/`loadDefaultImpulseResponse()` recorded IR A's new onset as the phase-alignment reference but never re-ran IR B's alignment against it, leaving an already-loaded IR B aligned to a stale, overwritten onset (reintroducing comb-filtering on the next Blend crossfade). `CabConvolutionEngine` now retains a copy of IR B's raw, pre-alignment buffer and automatically re-aligns it whenever IR A's reference onset changes.

### Other

- Housekeeping merged between v0.1.0 and this release, honestly summarised rather than omitted: branding/icon assets added and embedded in README/manual (#9, #14), a tag-triggered signed release CI workflow (#10), a marketing-copy reframe from "symphonic metal" to "heavy music" (#15), and a README fix pointing at the Releases page instead of a stale "no releases yet" note (#17).

## [0.1.0] - 2026-07-14

Expand Down
6 changes: 3 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ Per-repo working memory for Claude Code sessions on this plugin. Part of the **B
## What this is
Nave is the "cabinet IR loader / convolution (guitar & bass)" member of the suite. AU / VST3 / Standalone, JUCE 8.

## Status (v0.1.0 — M1 DSP completion & test coverage done)
Core DSP working, **50 Catch2 tests green**, CI (macOS + Windows, pluginval strictness 10 + auval) green. GUI is a functional v0.1 slider editor (custom LookAndFeel is roadmap M3). No signing yet (roadmap M4). M1 added IR Blend (dual-IR crossfade with inter-IR phase alignment) and simulated mic Distance; the "bundled IR library" part of M1's DSP issue is deferred (asset-sourcing/licensing, not a DSP task — see the issue comment on #1). Open work is tracked in this repo's GitHub **milestones/issues**.
## Status (v0.2.0 — M2 preset system + deep-dive voicing pass done)
Core DSP working, **73 Catch2 tests green**, CI (macOS + Windows, pluginval strictness 10 + auval) green. GUI is a functional v0.1/v0.2 slider editor plus a preset bar (custom LookAndFeel is still roadmap M3). No signing yet (roadmap M4). v0.2.0 shipped: a research-derived Distance taper revision (front-loaded low-shelf, see `docs/design-brief.md`), the suite's first M2 preset system implementation (`src/presets/`, pilot for the other 11 plugins - see `docs/preset-system-notes.md` for the replication recipe), 8 factory presets, a German frame-string localisation, and the app icon wired via `ICON_BIG`. The "bundled IR library" part of M1's DSP issue remains deferred (asset-sourcing/licensing, not a DSP task — see the issue comment on #1). Open work is tracked in this repo's GitHub **milestones/issues**.

## DSP
Nave is a cabinet IR loader built around two independent juce::dsp::Convolution slots (IR A, IR B), each constructed with the default zero-latency, uniformly-partitioned configuration (Convolution::Latency{0}), chosen because reamping IRs are short and the workflow is latency-sensitive. Signal chain: Convolution (crossfade of IR A/IR B via the IR Blend parameter) -> Distance (simulated mic-to-cab distance: proximity low-shelf + air-absorption high-shelf) -> LoCut HPF -> HiCut LPF -> juce::dsp::DryWetMixer (delay-compensated, currently 0 samples) -> Level trim. LoCut's minimum (20 Hz), HiCut's maximum (20 kHz), and Distance's minimum (0%) are each treated as explicit "off"/bypass positions where the engine skips that filter's IIR processing entirely (not just an extreme cutoff/gain) — this was necessary because even a 2nd-order Butterworth many octaves outside a test tone still imposes enough phase shift to fail a strict -80 dBFS sample-domain null; skipping it entirely gives a true bit-accurate passthrough at the default state (IR Blend defaults to 0%, i.e. IR A only, which is bit-identical to the pre-M1 single-IR path). Loading IR B applies inter-IR phase alignment (src/dsp/IrAlignment.{h,cpp}: onset detection via a relative-threshold crossing, then a time-domain shift) against IR A's most recently loaded onset, so blending the two never introduces comb-filtering from a timing mismatch. Both IR files' absolute paths are persisted as plain ValueTree properties on apvts.state (not APVTS float parameters), round-tripping automatically through copyState()/replaceState(); file I/O only ever happens off the audio thread (editor FileChooser callbacks or setStateInformation, both message-thread/session-load contexts).
Nave is a cabinet IR loader built around two independent juce::dsp::Convolution slots (IR A, IR B), each constructed with the default zero-latency, uniformly-partitioned configuration (Convolution::Latency{0}), chosen because reamping IRs are short and the workflow is latency-sensitive. Signal chain: Convolution (crossfade of IR A/IR B via the IR Blend parameter) -> Distance (simulated mic-to-cab distance: a front-loaded/"ease-out"-tapered proximity low-shelf + a linear directivity-darkening high-shelf, v0.2.0) -> LoCut HPF -> HiCut LPF -> juce::dsp::DryWetMixer (delay-compensated, currently 0 samples) -> Level trim. LoCut's minimum (20 Hz), HiCut's maximum (20 kHz), and Distance's minimum (0%) are each treated as explicit "off"/bypass positions where the engine skips that filter's IIR processing entirely (not just an extreme cutoff/gain) — this was necessary because even a 2nd-order Butterworth many octaves outside a test tone still imposes enough phase shift to fail a strict -80 dBFS sample-domain null; skipping it entirely gives a true bit-accurate passthrough at the default state (IR Blend defaults to 0%, i.e. IR A only, which is bit-identical to the pre-M1 single-IR path). Loading IR B applies inter-IR phase alignment (src/dsp/IrAlignment.{h,cpp}: onset detection via a relative-threshold crossing, then a time-domain shift) against IR A's most recently loaded onset, so blending the two never introduces comb-filtering from a timing mismatch. Both IR files' absolute paths are persisted as plain ValueTree properties on apvts.state (not APVTS float parameters), round-tripping automatically through copyState()/replaceState(); file I/O only ever happens off the audio thread (editor FileChooser callbacks or setStateInformation, both message-thread/session-load contexts).

## Build & test
```sh
Expand Down
26 changes: 25 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ set(CMAKE_OSX_DEPLOYMENT_TARGET "11.0" CACHE STRING "Minimum macOS deployment ta
# here at the top level avoids it being enabled implicitly deeper inside
# JUCE's CMake helpers, which has been observed to break Ninja's generate
# step (missing CMAKE_C_COMPILE_OBJECT rule variable).
project(Nave VERSION 0.1.0 LANGUAGES C CXX)
project(Nave VERSION 0.2.0 LANGUAGES C CXX)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
Expand Down Expand Up @@ -52,6 +52,28 @@ juce_add_plugin(Nave
NEEDS_MIDI_INPUT FALSE
NEEDS_MIDI_OUTPUT FALSE
IS_MIDI_EFFECT FALSE
ICON_BIG "docs/assets/icon.png"
)

# ==============================================================================
# Embedded assets: M2 preset system factory presets (presets/factory/*.json)
# and the M2 i18n frame's German translation (resources/i18n/de.txt). See
# src/presets/PresetManager.h and src/presets/Localisation.h - both consume
# these purely via BinaryData:: symbols passed in from PluginProcessor.cpp/
# PluginEditor.cpp, never by including BinaryData.h themselves, so they stay
# portable to sibling plugins (see docs/preset-system-notes.md).
# ==============================================================================

juce_add_binary_data(NaveBinaryData SOURCES
presets/factory/default.json
presets/factory/tameTheFizz.json
presets/factory/liveStage.json
presets/factory/darkVintage.json
presets/factory/pushedBackInTheRoom.json
presets/factory/touchOfRoomMic.json
presets/factory/evenBlend.json
presets/factory/parallelCabBlendedDry.json
resources/i18n/de.txt
)

# ==============================================================================
Expand Down Expand Up @@ -87,9 +109,11 @@ target_link_libraries(SharedCode INTERFACE
juce::juce_audio_formats
juce::juce_audio_utils
juce::juce_dsp
juce::juce_gui_basics
juce::juce_recommended_config_flags
juce::juce_recommended_lto_flags
juce::juce_recommended_warning_flags
NaveBinaryData
)

target_link_libraries(Nave PRIVATE SharedCode)
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ Nave is a cabinet impulse-response (IR) loader built on JUCE 8, aimed at reampin
- **IR loading, two independent slots** - load any WAV/AIFF impulse response into IR A and/or IR B via file choosers; loading happens off the audio thread and never blocks or allocates during playback
- **IR Blend** - crossfades between IR A and IR B (e.g. two cabs, or two mic positions on the same cab), with automatic inter-IR phase alignment so blending never introduces comb-filtering from a timing mismatch between the two IRs
- **Zero-latency convolution** - `juce::dsp::Convolution`'s zero-latency uniformly partitioned algorithm for both IR slots, so Nave never adds plugin delay compensation overhead
- **Distance** - simulated mic-to-cab distance (reduced proximity-effect bass + high-frequency air-absorption darkening as the value increases); an explicit "off" position at its default
- **Distance** - simulated mic-to-cab distance (a front-loaded proximity-effect bass cut plus high-frequency darkening - driven far more by loudspeaker directivity than literal air absorption at reamping distances - as the value increases); an explicit "off" position at its default
- **Presets** - 8 factory presets plus full user preset save/load/import/export (single files and zip banks), with German frame-string localisation
- **LoCut** - post-convolution high-pass, 20 Hz - 800 Hz (default 20 Hz, an explicit "off"/bypassed position), removes low-end mud
- **HiCut** - post-convolution low-pass, 2 kHz - 20 kHz (default 20 kHz, also an explicit "off" position), tames fizz
- **Mix** - dry/wet, default 100% (fully wet) - a cabinet IR is normally run fully in the signal path
Expand Down Expand Up @@ -58,7 +59,7 @@ Full musical context and usage tips: [`docs/manual.md`](docs/manual.md).
|---|---|---|
| M0 | Bootstrap - project skeleton, CI, docs | Done |
| M1 | DSP completion & test coverage - IR Blend, Distance emulation, inter-IR phase alignment, broadened Catch2 suite | Done (IR browser + bundled IR library deferred - see issue tracker) |
| M2 | Presets & state recall - preset system, factory presets | Planned |
| M2 | Presets & state recall - preset system, factory presets, DE frame localisation | Done |
| M3 | GUI & accessibility - custom LookAndFeel, accessibility pass | Planned |
| M4 | Release - code signing, notarization, installers, v1.0.0 | Planned |
<!-- ==END BODY== -->
Expand Down
Loading