Commit Graph

5 Commits

Author SHA1 Message Date
mikkeli 466ab478c9 chore: relicense to GPL-3.0-or-later and credit the ISC upstream
Commit 057d630 switched this repo from GPL-3.0 to Apache-2.0, which the
build contradicts: src/lib.rs calls nih_export_vst3! unconditionally and
nih_plug is taken with default features, so every build links vst3-sys —
GPLv3-or-later by its own authors' choice. deploy.ps1 then installs that
binary. The declared licence and the artifact's actual terms disagreed,
and the original GPL-3.0 was correct.

Given the intent to distribute non-profit and open-source, GPL costs
nothing here, so this restores it rather than dropping VST3 to keep a
permissive licence.

Also adds the ISC notice owed to Robbert van der Helm: the plugin
scaffolding derives from the NIH-plug cookiecutter template and example
plugins, whose one condition is that the copyright and permission notice
travel with all copies. The README credited the template under Reference
Material, which is courtesy, not compliance. ISC is GPL-compatible, so the
notice is retained in NOTICE rather than superseded.

Corrects the README's claim that Steinberg's SDK licence must be accepted
before distributing. No SDK source is vendored or redistributed, so there
is nothing to accept at build time. The binding constraint is vst3-sys's
GPLv3; Steinberg's trademark in "VST" is a separate regime that no source
licence resolves.

Copyright in the Irene <irene@novoyuuparosk.org> commits is held by the
repository owner (an agent identity), so no third-party agreement was
needed for this relicense.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WHt4DykxXaQnyaJfyS8KCn
2026-08-31 22:32:05 +09:00
Mikkeli Matlock b862f5a971 perf: skip the true-peak evaluation when it provably cannot reach the ceiling
The polyphase evaluation is PHASES*TAPS_PER_PHASE multiply-adds per channel
(96 for stereo) on every sample, unconditionally, and measured as 42 ns/sample
- 17% of the whole chain, and the largest single item in it. It is also the
only part that is pure throughput rather than a serial dependency chain, which
makes it the one worth attacking.

It is skippable. The interpolator is linear, so over its FIR support

    |true peak| <= peak_gain * max|history|,  peak_gain = max_phase sum |c|

and when that bound sits under the ceiling the true peak provably cannot reach
it. The limiter's gain target would be 1.0 for that sample either way, so
feeding the sample peak instead cannot change the outcome - a genuinely loud
neighbour still gets caught by its own evaluation.

- oversampler: split max_true_peak into push (cheap, must run every sample or
  the FIR history develops holes) and evaluate (the expensive half, skippable).
  Expose peak_gain and SUPPORT_LEN so a caller can compute the bound.
- limiter: track the sample peak over the FIR support with the SlidingMax
  already used for the look-ahead window, and skip evaluate when bounded under.

Measured, 48 kHz stereo:
  limiter, quiet 0.1 (skips)            66 -> 31 ns/sample
  limiter, hot 0.95 (always evaluates)  66 -> 72 ns/sample   (no real change)
  FULL CHAIN                           232 -> 195 ns/sample

So this pays off on material that is not pinned to the ceiling and costs
nothing when it is. On a maximizer the hot case is common, so treat the win as
situational rather than a flat 16%.

evaluate_never_exceeds_peak_gain_bound asserts the bound directly against
adversarial input (full-scale square waves, impulses), since the whole
optimisation rests on it, and also asserts it is not vacuously loose.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LU1NRL7fAaZTtFMTTPn2b1
2026-08-31 21:55:40 +09:00
Mikkeli Matlock 03d60a157b perf: remove per-sample waste from the compressor/limiter hot path
Found by reading the audio-thread code, not by profiling - no profiler is
set up in this project yet. All four are algorithmic or arithmetic wins,
none change the DSP result; the existing tests still pass.

- oversampler: max_true_peak recomputed its circular-history tap index
  (including a % TAPS_PER_PHASE) inside the phase x channel loop, though
  the index depends only on pos/k - 8x redundant. Hoisted to a per-sample
  precomputed index array. This runs unconditionally every sample (it
  feeds the limiter's true-peak detector, not gated by metering), so it
  was the highest-value fix.

- limiter: the sliding-window max rescanned the entire look-ahead ring
  (~73 samples at 48kHz/1.5ms) every sample - O(window) per sample.
  Replaced with an O(1)-amortized monotonic-deque sliding max, backed by
  preallocated ring arrays and RT-safe. Covered by a new test against a
  naive brute-force reference. Also precomputed the constant true-peak
  margin gain in prepare() instead of a powf every sample.

- compressor: replaced per-sample log10/powf(10, x/20) with the
  equivalent ln/exp forms, mirroring nih-plug's own gain_to_db_fast /
  db_to_gain_fast. Called 4x/sample (3 bands + 'All').

- both: replaced % capacity ring-index math (a real integer division,
  since capacity is not a power of two) with a conditional-subtract
  wrap_add helper. Left the oversampler's % TAPS_PER_PHASE alone - that
  divisor is a compile-time constant LLVM already turns into a multiply.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LU1NRL7fAaZTtFMTTPn2b1
2026-08-31 21:19:23 +09:00
irene 057d630c5b Switch license from GPL-3.0 to Apache-2.0
- Replace LICENSE with properly filled Apache-2.0 (Mikkeli Matlock 2026)
- Update Cargo.toml license field
- Add SPDX-License-Identifier headers to all .rs source files
2026-08-21 01:06:46 +09:00
Mikkeli Matlock 20c5a17a61 Stage 4b: true-peak limiting via 4x polyphase oversampling
Upgrades the brickwall limiter from sample-peak to true-peak (inter-sample).

- src/dsp/oversampler.rs: 4x polyphase windowed-sinc (4 phases x 12 taps, Blackman,
  each phase normalized to unity DC). Detection-only: max_true_peak() returns the
  inter-sample max magnitude and discards the upsampled samples; the audio path is
  untouched. Built in prepare(), no realtime allocation. Cost ~ one base-rate FIR
  per channel; its small group delay is absorbed by the limiter look-ahead, so no
  added reported latency.
- src/dsp/limiter.rs: detector peak = max(sample_peak, oversampler.max_true_peak());
  targets a 0.3 dB margin under the ceiling to cover the 4x detection residual.
- 16 unit tests (2 new: detects ~3 dB fs/4 inter-sample overshoot; preserves DC
  amplitude). README/docs updated: Stage 4 complete.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 15:50:06 +09:00