Status of each milestone, decisions made, and a short log per working session. Milestone definitions and "done when" criteria are in PLAN.md.
- Milestones 0–9 done and tested in Logic; v0.1.0 released (GitHub Release with .pkg and .dmg, built by CI).
- main is green in CI (build, 312 tests, auval, pluginval, packaging); no work in progress, no open decisions.
- To resume: read CLAUDE.md, PLAN.md and this file. New work starts as milestone 10 with a plan and the owner's
decisions first. Releases: bump the version in CMakeLists.txt, commit, push a matching
v*tag. - Housekeeping: pushes and tags go from IntelliJ (the terminal has no GitHub credentials); quit Logic before
testing a new build;
python3 tools/check_docs.pyafter documentation changes.
| # | Milestone | Status | Notes |
|---|---|---|---|
| 0 | Toolchain | Done | Command-line build, tests, pluginval and auval pass; owner confirmed CLion build and AU load in Logic (Standalone run not reported separately) |
| 1 | One bell band | Done | Knobs, smoothing, state save/load and measured response done; tests, pluginval and auval pass. Owner listening check in Logic: no clicks (session save/reopen not reported separately) |
| 2 | Full band set, tier 1 | Done | All 5 stages built and validated (tests Debug + Release, pluginval, auval). Owner listening check in Logic: no clicks (other checklist items not reported separately) |
| 3 | Response curve display | Done | Built and validated (108 tests, pluginval, auval); owner reviewed the layout and colours |
| 4 | Interactive display | Done | Built and validated (135 tests, pluginval, auval). Owner re-check in Logic: everything tested, looks good |
| 5 | Spectrum analyzer | Done | Built and validated (157 tests, pluginval, auval); owner tested in Logic |
| 6 | Per-band stereo | Done | Built and validated (172 tests, pluginval, auval); owner tested in Logic |
| 6b | Presets | Done | Built and validated (186 tests, pluginval, auval); owner tested in Logic |
| 7 | Dynamic EQ | Done | Built and validated (223 tests, pluginval, auval); owner tested in Logic except the side-chain (covered by unit tests only). Peak detector reading still an open decision |
| 8 | Linear phase mode | Done | Built and validated (245 tests, pluginval, auval); owner tested in Logic |
| 9 | Deferred features | Done | 9a A/B, 9b undo/redo, 9c peak pick, 9d EQ Sketch, 9e EQ Match, 9g spectral dynamics done (owner tested in Logic); 9f skipped; MIDI Learn dropped |
Status values: Not started · In progress · Done · Skipped
Newest first. Move each item here from "Open decisions" in CLAUDE.md once it is made.
| Date | Decision | Options considered | Reason |
|---|---|---|---|
| 2026-09-28 | Look and feel: the overall layout follows the reference EQ (full-window display, dB scale and meter on the right, thin top and bottom bars, band panel over the lower display); components and styling our own; no names, logos or copied assets | Own look with functional conventions; close imitation kept private; close imitation in the public repo; decide later with neutral styling | Chosen by owner (option 3, limited to the overall layout). Trade-dress risk noted at decision time |
| 2026-09-28 | Colours similar in character to the reference; values chosen by us, not sampled from the reference image | Own palette; close to the reference | Chosen by owner |
| 2026-09-28 | M3 band controls: reference-style band panel at the bottom of the display, with 16 band tabs until M4's click-to-select | Panel with tabs; display with the M2 grid below | Chosen by owner |
| 2026-09-28 | Double-click on empty display space adds a Bell (first free band, at the clicked frequency and gain); with all 16 in use nothing is added and a message is shown | Always Bell; type by position | Chosen by owner |
| 2026-09-28 | Dynamic gain law selectable per band: Range (full range 12 dB above threshold, smoothstep knee) or Ratio ((level - threshold)(1 - 1/ratio), 6 dB soft knee, capped at the range, sign of the range) | Threshold + range; ratio + range cap; selectable | Chosen by owner (law details proposed in planning) |
| 2026-09-28 | Dynamic types: Bell, Low Shelf, High Shelf | Bell + shelves; all gain types; bell only | Chosen by owner |
| 2026-09-28 | Detector listens to the band's region (bell: band pass at f/Q; low shelf: lowpass; high shelf: highpass) or, per band, the external side-chain filtered the same way; a band set to side-chain with no side-chain connected falls back to its own region | Region + side-chain; region only | Chosen by owner (fallback proposed in planning) |
| 2026-09-28 | Detector Peak or RMS (10 ms), selectable per band; attack/release in the dB domain | Peak; RMS; selectable | Chosen by owner |
| 2026-09-28 | Dynamic bands recompute their filters every 16 samples | 16; 32; every sample | Chosen by owner |
| 2026-09-28 | Auto Gain uses static gains only (dynamic movement ignored) | Static only; follow live gain | Chosen by owner |
| 2026-09-28 | Stereo dynamic bands detect and act per channel (a Stereo band can then give L and R different gains) | Linked; per channel | Chosen by owner |
| 2026-09-28 | Display shows live dynamic gain plus the range | Live gain + range; range only | Chosen by owner |
| 2026-09-28 | Dynamic parameters per band (hint 4): dyn, dynmode, thresh (-60..0 dB), range (-24..+24 dB), ratio (1-20), attack (0.1-200 ms), release (5-2000 ms), detector, sidechain; 259 parameters in total | - | Proposed in planning, accepted by owner |
| 2026-09-28 | Factory presets (11): Lead Vocal, Male Vocal, Female Vocal, Acoustic Guitar, Electric Clean, Electric Rhythm, Bass DI, Kick, Snare, Overheads, Mix Bus Polish. Frequencies and cut/boost directions from public mixing guides (at least two per preset); most dB and Q values are conservative choices, not from sources | All 13 proposed; drop the two weakest (Drum Bus, Piano); edit the list | Chosen by owner: drop Drum Bus and Piano |
| 2026-09-28 | Preset sources are not cited in the repo (the brand-name rule would otherwise need an exception) | Rule exception for citations; titles only; no citations in the repo | Chosen by owner |
| 2026-09-28 | Presets become milestone M6b, built before M7 (moved from the M9 list) | Now before M7; after M8; in M9 | Chosen by owner |
| 2026-09-28 | A preset stores all 16 bands (in use, enabled, settings, channel) plus output gain, Auto Gain and invert; not view settings | Bands and output; bands only; everything | Chosen by owner |
| 2026-09-28 | User presets are XML files in ~/Library/Audio/Presets/CatastrophicCoder/ParametricEQ/ | Preset files; host presets only | Chosen by owner |
| 2026-09-29 | M8 phase mode (Zero latency / Linear phase) is a hidden state property, not a host parameter | Host parameter; hidden state property | Chosen by owner |
| 2026-09-29 | M8 filter length is a tap count, the same at every sample rate (latency in ms shrinks at higher rates) | Same ms at every rate; same tap count | Chosen by owner |
| 2026-09-29 | M8 latency menu: 8192 / 16384 / 32768 taps (85 / 171 / 341 ms at 48 kHz) | 4 steps 4k-32k; 5 steps 2k-32k; 3 steps 4k/16k/64k; 3 steps 8k-32k | Chosen by owner |
| 2026-09-29 | M8: dynamic bands run as normal (IIR) filters after the linear-phase FIR, as in Zero latency mode | IIR after FIR; static part in FIR plus delta filter; unavailable in Linear phase | Chosen by owner |
| 2026-09-29 | M8: dynamic bands detect on the delayed signal (where they act) | Delayed signal; undelayed input (lookahead) | Chosen by owner |
| 2026-09-29 | Detector (Peak and RMS): classic linear-domain attack/release follower, converted to dB afterwards; replaces the dB-domain smoothing of 2026-09-28 | Keep; peak hold + dB smoothing; classic follower; decide later (RMS: both or Peak only) | Chosen by owner (both Peak and RMS) |
| 2026-09-29 | M8 FIR design: frequency sampling of the zero-phase 2x2 matrix on a grid 4x finer than the filter, inverse FFT, centred, 4-term Blackman-Harris window (Harris 1978); tap 0 zero, exactly symmetric, latency N/2 | - | Planned |
| 2026-09-29 | M8 accuracy bounds: a band is resolved when its frequency and (bell, notch, band pass) bandwidth f0/Q are at least 32 FFT bins (32 fs/N); for resolved bands above 32 fs/N: smooth shapes within 0.1 dB (worst measured 0.048 dB, 1 kHz Q8 notch), cuts within 0.5 dB outside their transition band (max(1/4, 24/slope) octaves) with a -40 dB floor (worst measured 0.000 dB); smooth-shape floor -60 dB. 93 of 126 grid cases resolved | - | Proposed (new bounds) |
| 2026-09-29 | M8 convolution: own uniformly partitioned overlap-save engine (512-sample partitions) instead of juce::dsp::Convolution, whose new filters start with an empty history (a dropout of up to half the filter length on every change); the input spectra are kept across filter changes and old/new outputs crossfade over 1024 samples. Adds 512 samples: latency = taps/2 + 512 | juce::dsp::Convolution; own engine | Chosen during implementation after a failing test (reported to owner) |
| 2026-09-29 | M8 mode and length switches: fade out (20 ms), switch, stay silent until the filter of the new length is in use and has a full input history (taps samples), fade in (20 ms). prepareToPlay and reset() count as a fresh stream (no wait) | - | Planned |
| 2026-09-29 | MIDI Learn dropped: MIDI input would change the AU type from aufx to aumf (breaking saved Logic sessions), and VST3 would need hidden controller parameters; hosts map controllers to parameters themselves | Drop; VST3 and Standalone only; change the AU type; postpone | Chosen by owner |
| 2026-09-29 | M9 split into 9a A/B, 9b undo/redo, 9c Spectrum Grab, 9d EQ Sketch, 9e EQ Match, 9f natural-phase-style mode, 9g spectral dynamics, built in that order | Small to large; workflow first; DSP first | Chosen by owner |
| 2026-09-29 | A/B slots hold everything: all parameters, bands in use, phase mode and length, view settings, current preset | Same as a preset; preset plus phase mode; everything | Chosen by owner |
| 2026-09-29 | Both A/B slots and the active one are saved with the session (state version 5); older sessions start with both slots equal | Saved with session; only while open | Chosen by owner |
| 2026-09-29 | Undo/redo covers sound edits only (bands, output, add/delete/enable, phase mode); not preset loads, A/B switches, view settings or host automation | Sound only; plus presets and A/B; everything | Chosen by owner |
| 2026-09-29 | A preset load or A/B switch clears the undo history | Clear; one history per slot; keep one history | Chosen by owner |
| 2026-09-29 | Undo history lives only while the plugin is open (not saved with the session) | Only while open; saved | Chosen by owner |
| 2026-09-29 | Undo/redo through two buttons in the top bar only; no keyboard shortcuts (Cmd-Z stays with Logic) | Buttons and shortcuts; buttons only; shortcuts only | Chosen by owner |
| 2026-09-29 | Peak pick (9c): a marker on the nearest spectrum peak appears while hovering empty display space; dragging it creates a band | Hover markers; modifier + drag; pick button | Chosen by owner |
| 2026-09-29 | Peak pick band: Bell at the peak frequency, Q from the peak's -3 dB width (0.5-18), gain set by the drag from 0 dB | Q from width; fixed Q 6; fixed Q 1 | Chosen by owner |
| 2026-09-29 | Peak pick follows the shown spectrum: pre when Pre or Pre+Post is shown, post when only Post; none with the analyzer off | Pre; post; whichever is shown | Chosen by owner |
| 2026-09-29 | Peak pick ring holds its peak and place while the pointer stays within the half-octave window it was chosen from (first implemented as 30 px, which failed while approaching); a press within 30 px picks it (owner feedback: the ring jumped with the live peak) | Hold while near; slow peak memory; both | Chosen by owner |
| 2026-09-29 | EQ Sketch (9d): Option + drag on empty display space draws a curve; release turns it into bands | Sketch button; Option + drag; right-click menu | Chosen by owner |
| 2026-09-29 | EQ Sketch replaces the bands whose frequency lies inside the drawn range; bands outside stay | Keep, use free slots; replace all; replace in drawn range | Chosen by owner |
| 2026-09-29 | EQ Sketch fitter may use bells, shelves and cuts | Bells only; bells + shelves; bells, shelves, cuts | Chosen by owner |
| 2026-09-29 | EQ Sketch uses all available slots (free plus replaced) for the closest fit | Fewest for 1 dB; fewest for 0.5 dB; all available slots | Chosen by owner |
| 2026-09-29 | Curve fitter bounds: single band within 0.01 dB (worst 4e-11), combinations of the fitter's own shapes within 0.1 dB above -40 dB (worst 0.038, bells + 24 dB/oct high cut), outside a partial range within 0.5 dB (worst 0.357); a drawn hump within 1 dB at its top | - | Proposed (new bounds) |
| 2026-09-29 | EQ Sketch keeps using all available slots (revisited after a full-width sketch filled all 16 slots) | Keep all slots; fewest for 1 dB; fewest for 0.5 dB | Chosen by owner |
| 2026-09-29 | Peak pick rings stay visible with all 16 bands in use; pressing one shows "All 16 bands in use" | Hidden; shown, press explains | Chosen by owner |
| 2026-09-29 | Peak pick shows rings on the 5 most prominent peaks of the shown spectrum while the pointer is over the display; nodes do not hide them; a ring within half an octave of the pointer holds still (replaces the single ring near the pointer) | One near the pointer; top peaks while hovering; near pointer, also over nodes | Chosen by owner |
| 2026-09-29 | EQ Match reference: the side-chain when connected (reference and current learned in one pass), otherwise two capture passes through the plugin input | Side-chain; capture; audio file; side-chain or capture | Chosen by owner |
| 2026-09-29 | EQ Match learns long-term average spectra between Learn and Stop; the match is computed once from the two averages | Average while learning; live match | Chosen by owner |
| 2026-09-29 | EQ Match asks on Apply whether to replace all bands or keep them and use the free slots | Replace all; keep, use free slots; ask each time | Chosen by owner |
| 2026-09-29 | EQ Match controls: Amount (0-100 %) and Smoothing (1/12 to 1 octave) | Amount + smoothing; amount only; none | Chosen by owner |
| 2026-09-29 | EQ Match controls in a separate floating window opened by a Match button; the target curve is previewed on the display | Panel over display; separate window; bottom bar strip | Chosen by owner |
| 2026-09-29 | EQ Match bound: match curve and applied bands within 0.25 dB of a known EQ's shape, 100 Hz - 10 kHz, for 6-8 s of pink noise at 1/6-octave smoothing (worst measured 0.057 dB) | - | Proposed (new bound) |
| 2026-09-29 | EQ Match learned spectra live in the plugin (not the window) and are saved with the project (state version 6: an "EQMatch" element with both spectra as 1024 log-spaced float32 points, 20 Hz - 20 kHz, plus Amount and Smoothing; about 11 kB); older sessions open with nothing learned | Until plugin removed; saved with project | Chosen by owner (1024-point form proposed after correcting a size estimate of about 10 kB for raw bins, which would be about 45 kB) |
| 2026-09-29 | Closing the plugin window stops a running EQ Match learn pass; what it learned is kept | Learning stops; keeps learning (background thread) | Chosen by owner |
| 2026-09-29 | User guide as an interactive web page (docs/guide/index.html, served by GitHub Pages from /docs): annotated screenshot with hotspots, use-case walkthroughs in tabs, gestures and display reference | Interactive web page; Markdown guide; both; in-plugin help | Chosen by owner |
| 2026-09-29 | The README's development-process section becomes a short note; details move to docs/DEVELOPMENT.md (superseded below) | Move to dev docs; keep in README; remove | Chosen by owner |
| 2026-09-29 | 9f natural-phase-style mode skipped: measured, Zero latency bells are already within 1.6-9.2 deg of the analog prototype's phase for 1-10 kHz bands at 48 kHz (28 deg only for a 15 kHz band, near Nyquist; about half at 96 kHz) | Analog-phase mode; reduced-phase mode; skip | Chosen by owner |
| 2026-09-29 | Spectral dynamics (9g) is a Spectral switch on dynamic bands: region, threshold, range, attack and release act per frequency slice | Option on dynamic bands; separate global processor | Chosen by owner |
| 2026-09-29 | Spectral latency (2048 samples) is reported only while at least one band is spectral; switching on/off fades like a phase-mode change | Added while in use; Linear phase only; always reported | Chosen by owner |
| 2026-09-29 | Each spectral slice compares its own level with the band's absolute threshold | Absolute; relative to neighbours | Chosen by owner |
| 2026-09-29 | Spectral engine: 2048-point frames, hop 512, square-root periodic Hann analysis and synthesis (exact delay of 2048 samples with no gain change); per-slice linear attack/release follower; band region weighting = power response of the M7 detector filter (bell band pass, shelf Butterworth low/high pass); side-chain slices drive the same slices; Stereo per channel, M/S around the slice | - | Planned |
| 2026-09-29 | Spectral switch as its own parameter band_spectral (bool, hint 5; 275 parameters), not a third Peak/RMS choice (which would shift stored automation of that menu); preset format 3 stores it, format 2 loads it off | - | Planned |
| 2026-09-29 | The README, DEVELOPMENT.md and user guide do not describe how the project is developed (the README note was removed; DEVELOPMENT.md keeps a neutral Workflow section) | - | Chosen by owner |
| 2026-09-29 | Release builds are ad-hoc signed (no Apple Developer ID; first use needs Open Anyway) | Ad-hoc; Developer ID + notarized | Chosen by owner |
| 2026-09-29 | Release builds are Apple silicon only (arm64), macOS 12 or later | Universal; Apple silicon only | Chosen by owner (minimum macOS 12 proposed in planning) |
| 2026-09-29 | Downloads: a .pkg installer (AU, VST3, Standalone selectable) and a .dmg with the Standalone app | Installer + disk image; installer only; zip of bundles | Chosen by owner |
| 2026-09-29 | CI keeps the packages as run artifacts; a v* tag also publishes a GitHub Release (via the runner's gh CLI, no third-party action) | Artifacts + tag releases; artifacts only | Chosen by owner |
| 2026-09-29 | First release version 0.1.0 | 1.0.0; 0.9.0; 0.1.0 | Chosen by owner |
| 2026-09-28 | Plugin name "Spectral Fault", brand (company) "Catastrophic Audio" | Name lists proposed in planning | Chosen by owner |
| 2026-09-28 | Rename details: bundle ID com.catastrophicaudio.spectralfault; CMake target SpectralFault (tests SpectralFaultTests); plugin codes, saved-state tag and preset tag unchanged; rename committed under M7 | Keep or change bundle ID; keep or rename target; M7 or separate prefix | Chosen by owner |
| 2026-09-28 | User preset folder moves to ~/Library/Audio/Presets/Catastrophic Audio/Spectral Fault/; the old folder's presets are copied once (only if the new folder has none); old files stay | Keep old path; move without migration; move and migrate | Chosen by owner |
| 2026-09-28 | Presets are listed in the plugin's own browser only (not exposed as host programs) | Plugin browser only; also as host programs | Chosen by owner |
| 2026-09-28 | Per-band channel mode Stereo / Left / Right / Mid / Side (band_channel, hint 3, default Stereo); Mid/Side encoded and decoded around each M/S band | - | Planned: per-band transform so L/R and M/S bands can be mixed in one chain |
| 2026-09-28 | Auto Gain with channel modes: exact 2x2 transfer-matrix model (power gain | M | |
| 2026-09-28 | Summed curve with channel modes: one curve for Stereo only; L and R curves with L/R bands; M and S curves with M/S bands only; mixed: L and R from the matrix diagonal (approximate) | Two curves as needed; one curve with all bands; one curve with Stereo bands only | Chosen by owner |
| 2026-09-28 | Nodes show their channel mode as a letter badge (L/R/M/S, none for Stereo) | Letter badge; panel and menu only | Chosen by owner |
| 2026-09-28 | Analyzer modes Off / Pre / Post / Pre+Post (default Pre+Post) | Selectable four modes; post only; pre+post always | Chosen by owner |
| 2026-09-28 | Analyzer slope compensation 4.5 dB/oct, pivot 1 kHz | 4.5; 3; none; selectable | Chosen by owner |
| 2026-09-28 | Analyzer dB range selectable 60 / 90 / 120 dB (default 90) | 0 to -90 fixed; 0 to -120 fixed; selectable | Chosen by owner |
| 2026-09-28 | Output meter added in M5: stereo peak and RMS (300 ms), peak hold 1 s then 20 dB/s, -60 to 0 dBFS, clip light | With M5; later | Chosen by owner; meter details proposed in planning |
| 2026-09-28 | Analyzer settings (mode, resolution, speed, range) saved with the session as state properties; freeze not saved | - | Follows the display-range decision |
| 2026-09-28 | Bands have three states: free, in use and enabled, in use and disabled. Delete (key or menu) frees a band; On/Disable/double-click only toggles enabled. Disabled bands are grey, bypassed, not editable until re-enabled. Replaces the M4 behaviour where every "off" removed the band | Found by the owner in the M4 check: disable and delete were the same | Owner's specification |
| 2026-09-28 | "In use" stored as hidden state properties band_used (not host parameters), with an atomic copy for the audio and Auto Gain threads; state version 3, older sessions: enabled bands count as in use | Hidden parameter; state property | Chosen by owner |
| 2026-09-28 | Double-click on a node toggles enable/disable (replaces "disables") | Toggle; nothing | Chosen by owner |
| 2026-09-28 | Disabled bands: grey node and grey curve outline, no fill, not in the sum | Grey node and curve; grey node only | Chosen by owner |
| 2026-09-28 | Double-click on a node disables that band (superseded: now toggles) | Disable; reset gain; nothing | Chosen by owner |
| 2026-09-28 | Band tabs removed in M4: bands are selected on the display; the band panel shows the primary selection and hides when nothing is selected | Remove tabs; keep tabs too | Chosen by owner |
| 2026-09-28 | Nodes of types without gain (cuts, notch, band pass, all pass) sit on the 0 dB line | 0 dB line; on the band's curve | Chosen by owner |
| 2026-09-28 | Band colours follow the visible spectrum: band 1 dark violet (390 nm) to band 16 red (645 nm), evenly spaced in wavelength (Bruton 1996); tab numbers use a lightened tint (WCAG contrast >= 4.5) | End at 645 nm; keep 390-700 nm (bands 14-16 identical red); even in hue. Tabs: lighter tint; exact band colour | Chosen by owner; replaces the golden-ratio hues |
| 2026-09-28 | Text scaling with the window: not needed for now | Scale fonts and bar heights; leave | Owner: not an issue for now |
| 2026-09-28 | Display range (3/6/12/30 dB) saved with the session as a non-automatable state property | Save in session; not saved | Chosen by owner |
| 2026-09-28 | M2 editor: one resizable row (1000x360 to 2600x800, default 1480x440) | One row of 16; two rows of 8; one resizable row | Chosen by owner |
| 2026-09-28 | Controls a type does not use are greyed out | Grey out; hide | Chosen by owner |
| 2026-09-28 | Release CPU measured as part of M2 validation and logged | Measure and log; skip | Chosen by owner |
| 2026-09-28 | Auto Gain formula: K-weighted (BS.1770-5) pink-noise power average | Unweighted power average; K-weighted power; mean dB with clamp (mean dB breaks with deep cuts) | Chosen by owner |
| 2026-09-28 | Auto Gain excludes Low Cut and High Cut | All enabled bands; exclude cuts | Chosen by owner |
| 2026-09-28 | Auto Gain limit +-24 dB | +-12; +-24; +-30 dB | Chosen by owner |
| 2026-09-28 | Auto Gain computed on a background thread | Background thread; message-thread timer; coarse on the audio thread | Chosen by owner. Implementation polls parameters every 20 ms instead of using listeners (listeners can fire on the audio thread and depend on the host's notification path) |
| 2026-09-28 | Band defaults: per-type presets, all disabled (1 Low Cut 30 Hz, 2 Low Shelf 80 Hz, 3-14 Bells 120 Hz-12 kHz log-spaced, 15 High Shelf 10 kHz, 16 High Cut 18 kHz; 0 dB; bells Q 1, others Q 0.71; cuts 24 dB/oct) | Off and spread; off at 1 kHz; off with per-type presets | Chosen by owner |
| 2026-09-28 | Band 1 is disabled in a new instance (a new instance is a bit-exact pass-through); M1 sessions migrate to band 1 = enabled bell | Enabled bell at 0 dB; disabled like the rest | Chosen by owner |
| 2026-09-28 | CPU check is an always-on test: 16 Brickwall bands at 96 kHz stereo must run faster than real time in any build | Hidden Release-only benchmark; always-on; skip | Chosen by owner |
| 2026-09-28 | Response-test bounds vs analog, extended grid (fc / +-1 oct below 0.8 Nyq / +-1 oct at or above 0.8 Nyq), with worst measured: Bell 0.1 / 0.65 (0.614) / 2.7 (2.640); Band pass 0.1 / 0.7 (0.645) / 1.4 (1.378); Notch depth < -100 dB / 0.55 (0.538) / 1.8 (1.754); Shelves 0.15 (0.123) / 0.1 (0.080) / 0.25 (0.248); Tilt 0.4 (0.385) / 0.25 (0.222) / 0.05 (0.024); Low Cut 0.1 / 0.5 (0.454); High Cut 0.1 / 0.3 (0.277) / 12.25 (12.218); All pass flat within 1e-9 dB; Flat Tilt 0.25 (0.224) from the ideal line. Strict-grid bounds kept as well (Bell 0.35) | Accept and keep strict grid too; accept extended only; drop the new rows | Chosen by owner after tightening attempts |
| 2026-09-28 | Extended test grid: add f0 = 10 and 16 kHz at 44.1 kHz; separate near-Nyquist bounds for points at or above 0.8 Nyquist | Add rows or keep CLAUDE.md grid; near-Nyquist bounds or leave those points unasserted | Chosen by owner |
| 2026-09-28 | Cut slope test: points pass if digital and analog are both below -120 dB | Test below 0.8 Nyquist only; -120 dB floor; add a Nyquist zero to the design | Chosen by owner |
| 2026-09-28 | High Cut sections: Vicanek 2016 section 4.1 as published | 4.1; three-point section matched at 2 fc; three-point section matched at Nyquist | Chosen by owner; 4.1 is valid everywhere, 3-point at 2 fc was closer below 0.8 Nyq but had no solution in 6/208 cascades |
| 2026-09-28 | Tilt shelf matching point f_m = 0.9 (paper) | 0.9; 0.84 fitted to our grid | Chosen by owner |
| 2026-09-28 | Brickwall = order-32 Butterworth cascade (192 dB/oct, 16 sections) | Steep Butterworth; elliptic IIR; defer to M8 FIR | Chosen by owner |
| 2026-09-28 | Q has no effect on cuts (always Butterworth) | Ignored; resonance on last section; scale all sections | Chosen by owner |
| 2026-09-28 | Flat Tilt: 16 octave-spaced matched one-pole shelves | 16 one-pole shelves; defer to M8 FIR | Chosen by owner |
| 2026-09-28 | M2 shapes: all tier-1 shapes (Flat Tilt and Brickwall need research first) | All tier-1; all but Flat Tilt/Brickwall; core only | Chosen by owner |
| 2026-09-28 | Shelves: matched two-pole Butterworth (Vicanek 2024/25), no Q | Matched Butterworth; RBJ with Q; matched plus one-pole 6 dB option | Chosen by owner |
| 2026-09-28 | Cut slopes in 6 dB steps (6-96 dB/oct) plus Brickwall | 12-dB steps; 6-dB steps; 12-dB steps plus 6 | Chosen by owner |
| 2026-09-28 | Type/slope/enable changes crossfade over ~20 ms; smoothing rule unchanged | Crossfade; switch and reset state; switch and keep state | Chosen by owner (first picked switch and reset, then crossfade to keep the rule) |
| 2026-09-28 | Auto Gain static, from the summed EQ curve | Static from curve; dynamic measured; defer | Chosen by owner |
| 2026-09-28 | M2 editor: grid of all 16 bands | Band selector and strip; grid; generic editor | Chosen by owner |
| 2026-09-28 | Phase invert: one global switch at the output | Not in M2; output invert; per-band invert | Chosen by owner |
| 2026-09-27 | Response tests are two-level: measured vs digital design within 0.1 dB; vs analog within 0.1 dB at f0 and within a stated bound at ±1 octave (bell: 0.35 dB) | Two-level check, loosen ±1 octave only, keep 0.1 dB and change the design, trim the test grid | Chosen by owner after the matched bell measured up to 0.317 dB off analog at +1 octave (wide cuts near f0/fs = 0.1) |
| 2026-09-27 | Project generation: CMake (juce_add_plugin) |
CMake, Projucer | Chosen by owner |
| 2026-09-27 | Filter topology: matched second-order designs (Vicanek 2016) in a biquad structure; tests compare against the analog prototype | RBJ biquad, TPT/SVF, matched, RBJ now and decide by M4 | Chosen by owner |
| 2026-09-27 | Unit tests: Catch2 as a git submodule in external/Catch2, pinned to a release tag |
Catch2 or GoogleTest, each via FetchContent or submodule | Chosen by owner |
| 2026-09-27 | Primary test host: Logic (AU); VST3 covered by pluginval only | Logic, Reaper, GarageBand | Chosen by owner |
| 2026-09-27 | Public open-source repo, licence AGPLv3 | Private, public AGPLv3, public permissive | Chosen by owner |
| 2026-09-27 | Target band count: 16 | 8, 16, 24, decide at M2 | Chosen by owner |
| 2026-09-27 | Milestone order after 5: per-band stereo, dynamic EQ, linear phase | Any order of 6–8, or decide later | Chosen by owner |
| 2026-09-27 | PLAN.md and PROGRESS.md live in docs/ |
Move to docs/, keep at root |
Chosen by owner |
Newest first. One entry per session, a few lines each.
- Done: CMake defaults arm64 / macOS 12, options SPECTRALFAULT_BUILD_TESTS and SPECTRALFAULT_COPY_PLUGIN; packaging/package.sh (Release build, ad-hoc signing, component packages with relocation off, installer with welcome page and licence, disk image with read-me); .github/workflows/build.yml (build, tests, auval, pluginval, package, artifacts; GitHub Release on v* tags after checking the tag against the CMake version); README "Download and install" and build badge; DEVELOPMENT.md packaging, releasing and CI sections.
- Local release build: SpectralFault-0.1.0.pkg (6.1 MB) and .dmg (2.7 MB); all three bundles arm64, minos 12.0, ad-hoc signatures verify; payload paths correct, relocation off; disk image holds the app and read-me. Extended attributes are cleared, but macOS's protected com.apple.provenance stays on locally built files and is stored as AppleDouble entries (restored as attributes on install). The CI-built package has none (checked: 0 AppleDouble entries, arm64, minos 12.0, ad-hoc signatures verify, relocation off).
- Found in a CI rehearsal (Release, tests in parallel): the linear-phase designer thread could design for the default 48 kHz before prepareToPlay and hand that filter over at another rate (heard until the right filter crossfaded in). It now waits for the host's rate; new test reproduces it (1.0 dB instead of 6 dB before the fix).
- Tests: 312/312 (Debug), and three parallel Release runs 312/312; pluginval strictness 5 (VST3 Debug and Release, AU) and auval pass.
- First CI run on GitHub: green (owner).
- v0.1.0 tagged (pushed from IntelliJ); CI published the GitHub Release "Spectral Fault 0.1.0" with SpectralFault-0.1.0.pkg and .dmg.
- Done: band panel "Spectral" switch (fifth row in the dynamics column, rows shrink to fit the smallest panel;
enabled only with Dynamic on; Peak/RMS greyed while spectral);
ResponseCurvesdraws a spectral band at static gain plus its live per-slice gain (recomputed when any point moves 0.05 dB), fed from the engine viagetSpectralGainDb; screenshot spectral-band.png; README, DEVELOPMENT.md and the user guide (dynamics hotspot, new "Spectral harshness control" walkthrough) updated. The first screenshot threshold (-48 dB) let the noise trigger the whole region; -30 dB shows the intended resonance-only cut. - Tests added / passing: 311/311. Panel switch both ways, greying, layout at three sizes; per-slice curve and the 0.05 dB recompute step; the display on real audio (loud slice pulled down, quiet slice not). pluginval strictness 5 (VST3, AU) and auval pass.
- Owner tested spectral dynamics in Logic; 9g done, and with it milestone 9 (all planned milestones complete).
- Done: parameter band_spectral (hint 5); preset format 3; processor runs the spectral engine after the bands
(and after the linear-phase FIR) while any band is spectral, the band's own filter keeps its static gain; the
mode switcher now also fades for spectral on/off and waits for one full frame; latency = linear-phase latency +
2048 while spectral, reported from a 10 Hz message-thread timer (
refreshLatency()), since the switch is an automatable parameter. - Found while testing: a test passed a 2-channel buffer with the side-chain bus enabled and the processor read side-chain channels that did not exist (crash). Hosts always pass all channels; the processor now uses the side-chain only when the buffer carries its channels.
- Tests added / passing: 308/308. Parameter and count; latency only while spectral, added to linear phase; measured delay = reported (zero latency and linear phase); per-slice action and static gain through the processor; side-chain ducking through the bus; clean on/off switching; presets format 3 and format 2 compatibility; no allocation with spectral bands switching. pluginval strictness 5 (VST3, AU) and auval pass.
- Next step: stage 3, panel switch, per-slice display, docs.
- Done:
SpectralDynamicsEngine(src/dsp) andBandSettings::Dynamics::spectral/isSpectral(); not yet wired into the processor (stage 2). - Tests added / passing: 300/300. Exact 2048-sample delay without spectral bands and below threshold; a loud tone cut by the range while a quiet tone in the same band is untouched; outside the region unchanged (< 0.05 dB); attack/release timing; side-chain slices duck only their own frequencies; per channel and Side-only behaviour; only active dynamic Bell/shelf bands with Spectral take part; no allocation (16 bands, side-chain, changing settings); 16 spectral bands at 96 kHz: Debug 301 ms, Release 43 ms per second of audio. pluginval strictness 5 (VST3, AU) and auval pass (plugin unchanged in use).
- Next step: stage 2, processor (parameter, latency and switching, presets format 3).
- Measured the phase of the matched bells against their analog prototypes (400 points up to 0.49 fs): worst differences 1.6 / 4.2 / 9.2 / 28 deg for 1 / 5 / 10 / 15 kHz bells at 48 kHz, all at the top of the range; about half at 96 kHz. An analog-phase mode would add little; owner chose to skip 9f.
- Next step: 9g spectral dynamics.
- README rewritten (features, screenshots, under the hood with sources, building, status, licence); new docs/DEVELOPMENT.md (prerequisites, build, tests incl. hidden runs, validation, troubleshooting, architecture diagram, source layout, state versions, testing approach, conventions, how the project is developed); interactive user guide docs/guide/index.html (15 hotspots on the interface, 9 walkthroughs, gestures, display legend, FAQ; dark/light, no external scripts), docs/index.html redirect and .nojekyll for GitHub Pages.
- Screenshots: hidden test "[.screenshots]" renders 8 scenes at 2x into docs/images (1.8 MB). The first EQ Match scene (two different resonances) gave an off-scale curve and was replaced by a realistic brighter reference.
- Checks: tools/check_docs.py (all relative links resolve), HTML tag balance and script syntax, headless renders at 1400 and 520 px (headless Edge will not go below a 492 px viewport).
- Owner to enable GitHub Pages (Settings > Pages > Deploy from a branch > main, /docs).
- Owner asked for learned spectra to survive closing the plugin window; decided: saved with the project, learning
stops when the window closes. Done:
MatchSessionmoved into the processor;SpectrumAveragersaves and restores 1024 log-spaced points (read-only once restored); "EQMatch" element saved beside the A/B element, state version 6; the panel shows restored Amount and Smoothing. - Tests added / passing: 290/290. Data survives closing and reopening the editor (preview shows again); closing during a pass stops it and keeps what was learned; save and reload give equal seconds and settings and the match curve within 0.01 dB; the element is under 16 kB; version 5 states open with nothing learned; A/B switches leave the learned data alone. pluginval strictness 5 (VST3, AU) and auval pass.
- Next step: owner's check in Logic, then 9f natural-phase-style mode.
- Done:
SpectrumAverager(src/dsp; 8192-point Hann frames, 50 % overlap, power averaged per bin, read with +-1/24-octave band averaging),MatchCurve(smoothing, reference - current, overall level removed, amount, +-24 dB),MatchSession(src/ui; learning passes, side-chain one-pass mode, apply via CurveFitter: replace all or keep existing with the free slots, one undo step),MatchPanel+MatchWindow(floating, always on top), bottom bar Match button, dashed preview on the display, side-chain tap FIFO fed only while learning.BandParameterWriter::writeFittedBandnow shared by EQ Sketch and EQ Match. - Found while testing: single-bin readings were noisy at low frequencies (a 1.2 dB outlier at 100 Hz after 8 s of white noise); levels now average the bins within +-1/24 octave, so steady tones read below their peak level (noise reads its density; matching compares like with like).
- Tests added / passing: 286/286. Averager: white flat, pink -3 dB/oct by regression, a tone stands out 40 dB; smoothing widths; match curve recovers a known bell + shelf within 0.05 dB, amount scales, level ignored; side-chain tap only while learning; capture/replace, keep-existing and side-chain flows within 0.06 dB, one undo step; window toggles from the bottom bar, preview appears and follows Amount; no allocation with the tap on. pluginval strictness 5 (VST3, AU) and auval pass. Snapshot of the panel (m9e_match_panel.png).
- Next step: owner's check in Logic, then 9f natural-phase-style mode.
- Owner, after restarting Logic: Option-drag works (a full-width sketch added nodes nicely); but afterwards no rings appeared until bands were deleted, and then only when the pointer met a peak. Causes, both by design: the sketch filled all 16 slots (rings were hidden with no free slot), and only one ring near the pointer, never over nodes, was shown. Owner decisions (Decisions table): keep all slots; show rings with full slots and explain on press; rings on the 5 most prominent peaks while hovering, nodes not hiding them, each held while the pointer is within half an octave.
- Tests rewritten for the rings (top 5 of the shown spectrum, kept over nodes, none with the analyzer off or the pointer outside; drag creates the bell; full slots: message, no band, no undo step; held ring while approaching; rings away from the pointer follow the spectrum). 278/278 tests, pluginval strictness 5 (VST3, AU), auval pass.
- A clang crash (segmentation fault) on one build was not reproducible; the rebuild succeeded.
- Owner retested in Logic; 9c and 9d done.
- Next step: 9e EQ Match.
- Owner: Option-drag did nothing; the ring still jumped while hovering. Logic had been started at 12:33, before the ring fix (12:39) and EQ Sketch (12:49) were installed, so it was still running the 9c build; Logic keeps a plugin's code until it quits.
- Separately, the ring fix itself was flawed: it held only within 30 px, but a ring can appear up to half an octave from the pointer, so approaching it re-picked peaks on every move. Now it holds while the pointer stays over empty space within the half-octave window it was chosen from; a press within 30 px picks it. New test: approaching the ring in steps while the spectrum changes leaves it in place (278/278 tests, pluginval and auval pass).
- Next step: owner retests both in Logic after restarting it.
- Done:
CurveFitter(src/dsp): end analysis (level end -> shelf, steep drop -> cut with slope from the drop), bells added at the largest remaining error until every slot is used, Levenberg-Marquardt refinement on the plugin's own designs; fitted with and without the end bands, closer fit kept (a broad bell's tail had been taken for a shelf plateau).ResponseDisplay: Option-drag on empty space draws (one height per pixel column, line shown while drawing); release replaces bands inside the drawn range, keeps the rest, fits the sketch minus the kept bands with outside points weighted 0.5 towards "add nothing"; one undo step. Sketches under 1/3 octave ignored. - Tests added / passing: 277/277. Slot count, stereo/enabled/no dynamics; single bells exact; three combinations with shelf/cut at the ends; partial range spill; 16-band fit Debug 324 ms, Release 42 ms; sketch replaces in range, keeps outside, curve within 1 dB of the hump, complete host edits, one undo step; plain drag selects; narrow sketch ignored. pluginval strictness 5 (VST3, AU) and auval pass. Snapshot of a sketch in progress (m9d_sketch.png).
- Observed: with "all available slots", a simple hump sketched with 15 slots gave 11 bands within 0.5 dB of 0 dB.
- Next step: owner's check in Logic, then 9e EQ Match.
- Done:
PeakFinder(src/ui): local maxima at least 3 dB prominent within an octave, Q from the -3 dB width;ResponseDisplay: marker (with frequency label) on the most prominent peak within half an octave of the pointer over empty space, from the shown spectrum; pressing it creates a Bell (peak frequency, estimated Q clamped 0.5-18, 0 dB) and the drag sets the gain only; creation and drag are one undo step. No marker over nodes, with the analyzer off or with all 16 bands in use. The UI uses the neutral name "peak pick" (brand rule). - Tests added / passing: 269/269. Finder frequency and Q (within 10 % for Q 1, 4, 12) and prominence; no peaks for flat spectra or a 2 dB bump; most prominent near the pointer; marker from pre or post by analyzer mode, none over nodes or with the analyzer off, on a real 1 kHz resonance fed through the plugin; drag creates the bell with gain following, frequency fixed, complete host edits, one undo step; all bands in use: no marker. pluginval strictness 5 (VST3, AU) and auval pass. Snapshot shows the marker.
- Owner feedback from Logic: the ring jumped with the live peak and could only be caught with the analyzer frozen. Fixed: the ring holds while the pointer is within 30 px, and a press in that radius picks it (270/270 tests, pluginval and auval pass). Awaiting the owner's check of the fix.
- Next step: 9d EQ Sketch.
- Done:
UndoHistory(src/presets): steps from the user's parameter gestures and explicit transactions (band in use, phase mode, display add/delete/enable/menu edits); sound-only snapshots via the new sharedSettingSnapshot(A/B now uses it too); undo/redo apply them as host edits; wheel/pinch Q steps on the same bands within 1 s merge; 100 steps; preset loads, A/B switches and session loads clear it. Top bar undo/redo buttons left of A/B. - Tests added / passing: 263/263. Knob/menu edit = one step; multi-node drag = one step; add, delete, disable and menu type change each one step; phase mode and length undoable with latency; Q wheel merge; automation, view settings and empty gestures not recorded; preset/A-B/session load clear; redo cleared by a new edit; 100-step cap; undo sends complete host edits for changed parameters only and records nothing; buttons enable, act and fit at three sizes. pluginval strictness 5 (VST3, AU) and auval pass.
- Owner tested undo/redo in Logic; 9b done.
- Next step: 9c Spectrum Grab.
- M9 assessed feature by feature: only MIDI Learn needed a change to existing work (AU type aufx -> aumf); dropped by owner. Order and A/B decisions recorded.
- Done:
AbComparison(src/presets): capture/apply of the complete setting, switch (differing parameters as host edits), copy, saved state; top bar A / B / copy buttons left of the preset browser, active slot in amber. - Tests added / passing: 253/253. Start state; each slot keeps its edits (bands, in-use, dynamics, output, phase mode, view); copy; host edits only for differing parameters, all complete; saved and restored with the active slot, exactly one A/B element in the saved state; older sessions give equal slots; loading a preset changes only the active slot; buttons switch, copy and show the active slot, laid out at three sizes. pluginval strictness 5 (VST3, AU) and auval pass.
- Owner tested A/B in Logic; 9a done.
- Next step: 9b undo/redo.
- Owner tested Linear phase mode in Logic and marked M8 done.
- Done:
PhaseModeControls(src/ui): Zero latency / Linear phase and a length menu showing each length's latency in ms at the current sample rate (96 / 181 / 352 ms at 48 kHz); the length menu is greyed in Zero latency mode. Placed on the right of the top bar instead of the planned bottom bar: at the minimum width (960 px) the bottom bar has no room for two more menus. - Tests added / passing: 245/245. Menus drive the processor and follow it (session load, sample rate change); layout and readability at three sizes, clear of the preset browser. Snapshots checked. pluginval strictness 5 (VST3, AU) and auval pass.
- Next step: owner's listening check in Logic (mode switch, the three lengths, parameter changes, latency compensation against another track).
- Done:
LinearPhaseEngine(own partitioned convolution, 2 or 4 paths),LinearPhaseUpdater(background thread, 20 ms poll, designs on change and hands filters over through a preallocated slot), processor wiring: hidden state properties linearPhase / linearPhaseLength (state version 4), latency taps/2 + 512 reported, static bands run as the FIR and dynamic bands as IIR after it, mode/length switches with fade and history wait. - Found while testing: juce::dsp::Convolution restarts each new filter with an empty history, so every parameter change in Linear phase mode silenced up to half the filter length; replaced by our own engine (Decisions). A length change also glitched because the fade-in began on a block still made by the old filter; readiness is now checked before processing. In Mid/Side chains the cross terms depend on phase, so they are compared with the zero-phase model rather than with Zero latency mode.
- Tests added / passing: 243/243 (three parallel runs). State and latency reporting; measured delay = reported at 44.1/48/96/192 kHz for all three lengths; symmetric impulse response; flat EQ = exact delay; null against Zero latency (Stereo/Left/Right) within 0.1 dB; Mid/Side chain matches the zero-phase model within 0.1 dB; parameter change reaches the audio in about 165 ms of wall-clock time without clicks; mode and length switches clean; dynamic bands act after the FIR; toggling dynamics clean; no allocation in processBlock while filters swap (the allocation counter now counts only the audio thread); CPU at 32768 taps with Mid/Side at 96 kHz: Debug 102 ms per second of audio, Release 9.5 ms. pluginval strictness 5 (VST3, AU) and auval pass.
- Known transient: toggling a band's dynamics in Linear phase mode briefly (until the next filter, about 70-170 ms) applies that band twice or not at all.
- Next step: M8 step 3, phase mode and length menus in the bottom bar.
- Done:
LinearPhaseDesigner(src/dsp): active, non-dynamic bands -> zero-phase 2x2 matrix sampled on a 4x oversampled FFT grid -> symmetric Blackman-Harris-windowed FIRs of 8192/16384/32768 taps (cross terms only with Mid/Side bands). Release: about 50 ms per 32768-tap stereo design (background thread in step 2). - Found while testing: a zero of a response (band pass or low cut at DC) could round to a slightly negative power and give NaN taps; clamped at zero. Accuracy depends on bandwidth, not only frequency: unresolved examples are a 100 Hz Q4 bell (0.57 dB off at 16384 taps, 48 kHz) and a 1 kHz Q8 notch (-22 dB deep instead of -59 at 8192 taps).
- Tests added / passing: 232/232. Tap counts and latency; flat EQ = pure N/2 delay; exact symmetry; accuracy for resolved bands at 44.1/48/96 kHz (bounds in Decisions); cut slope one and two octaves out; stereo matrix terms for all five channel modes; dynamic bands left out. Hidden diagnostic "[.diag]" prints the full accuracy table. pluginval strictness 5 (VST3, AU) and auval pass (audio path unchanged).
- Next step: M8 step 2, audio path (convolution, latency, background updates, mode switch).
- Decisions recorded (phase mode as state property; same tap count at every rate; 8k/16k/32k taps; dynamic bands as IIR after the FIR, detecting the delayed signal; classic follower for Peak and RMS).
- Done:
LevelDetectornow runs attack/release on the linear level and converts to dB afterwards. - Tests added / passing: 225/225. Attack/release time constants on the linear level; release falls 8.69 dB per release time (RMS: analytic value with its 10 ms mean square, -6.78 dB after the first 100 ms); at default 10/100 ms a steady sine reads within 1.3 dB of its peak (worst measured -1.16 dB), RMS reads the RMS. Stage 2 processing tests pass unchanged. pluginval strictness 5 (VST3, AU) and auval pass.
- Next step: M8 step 1, linear-phase FIR design.
- Owner tested dynamics in Logic and marked M7 done. The side-chain was not tried in Logic (routing not found); it is covered by unit tests only. The Peak detector reading stays an open decision in CLAUDE.md.
- Done: band panel dynamics section (Dynamic switch, Range/Ratio, Peak/RMS, Side-chain, Thresh, Range, Ratio, Attack, Release); the switch is active for Bell and shelves, its settings only while it is on, Ratio only in Ratio mode. Panel widened to 780-1240 px. Display: a dynamic band's curve follows its live gain (of L and R, the channel moving further), with a thin static curve and a faint area to static + range edged by a dashed line; the sum follows the live curves; curves recompute when a live change moves 0.05 dB or more. Preset format 2 stores dynamics ( per band); format 1 files load with dynamics off; factory presets unchanged (dynamics off).
- Tests added / passing: 223/223. Preset format 2 round trip, format 1 compatibility, clamping, capture/apply and modified state; live, static and range curves, 0.05 dB recompute step, non-dynamic bands ignore live gain; panel attachments both ways, greying rules, layout and menu readability at three sizes; display curve at the processor's live gain after real audio. pluginval strictness 5 (VST3, AU) and auval pass. Snapshots checked at three sizes.
- Simplification: a Stereo band whose L and R move differently is drawn with the larger change, not as split curves.
- Next step: owner's listening check in Logic; Peak detector decision (open).
- Done: product "Spectral Fault", company "Catastrophic Audio", bundle ID com.catastrophicaudio.spectralfault, CMake target SpectralFault and test binary SpectralFaultTests; the top bar shows the new name (JucePlugin_Name). Codes Ctcd/Peq1 and the state and preset XML tags unchanged. User presets move to the new folder, with a one-time copy from the old one.
- Tests added / passing: 211/211 (new and old folder paths; copy once, skip when the new folder has presets, nothing without an old folder; temporary folders never migrate). pluginval strictness 5 passes on both new bundles.
- Removed the old installed ParametricEQ.component/.vst3 (owner's OK) and restarted the AU registry;
auval -alists "Catastrophic Audio: Spectral Fault" and auval passes.
- Done: dynamic parameters reach the bands; optional "Sidechain" input bus (disabled by default; disabled, mono or
stereo);
CascadeProcessorper-channel coefficients;EqBanddynamic path: detector filter (bell band pass, shelf lowpass/highpass at Q 0.71) and aLevelDetectorper detection channel, own input or side-chain (falls back to own input when none is connected), gain redesigned every 16 samples (per channel for Stereo bands, only when the change moves), threshold/range/ratio smoothed over 20 ms; live gain change per band and channel for the UI. - Tests added / passing: 209/209. Steady-state cut and boost (Range, Ratio), bit-exact below threshold, attack and release timing, frequency selectivity, shelf detector regions, per-channel and Side detection, side-chain bus layouts, side-chain triggering and fallback, bursts and sweep (finite, step < 0.2), reset(), no allocation with 16 dynamic bands plus side-chain. CPU, 16 dynamic stereo bells at 96 kHz: Debug 137 ms per second of audio, Release 18.7 ms. pluginval strictness 5 (VST3, AU) and auval pass; auval lists the side-chain bus.
- Found: the Peak detector smooths |x| in the dB domain, so a steady sine reads below its peak: at -10 dBFS, 1 kHz, -10.9 dB with 1/50 ms and -12.4 dB with the default 10/100 ms (RMS reads -13.0). Range-mode tests saturate and do not show it; the Ratio test now checks the law against the detector's actual reading. Raised with the owner.
- Next step: stage 3, panel dynamic controls, live gain and range on the display, preset format v2.
- Done: processor
reset()(band filters, crossfades, output ramp);LevelDetector(Peak/RMS, attack/release in dB);DynamicGainLaw(Range and Ratio); nine dynamic parameters per band (hint 4), 259 parameters in total, not yet used by the audio path. - Tests added / passing: 194/194. Detector time constants and readings; both gain laws (thresholds, knees, slope, cap, sign); parameters and defaults; pre-M7 sessions load with dynamics off; reset() stops a ringing tail. pluginval strictness 5 (VST3 in 4 s, AU) and auval pass.
- Resolved: the missing reset() noted in M6.
- Next step: stage 2, dynamic gain in EqBand (per channel, every 16 samples), detector filters, side-chain bus.
- Research: EQ starting points were collected for common sources from public mixing guides (at least two per instrument, with quotes). Frequencies and directions were well sourced; most dB and Q values were not, and were chosen conservatively. Owner kept 11 of 13 proposals and chose not to cite the sources in the repo.
- Done:
Preset,FactoryPresets,PresetManager(src/presets); preset state in the session; top-bar browser (previous/next, name with "*", categories, User, Save As, Delete). - Tests added / passing: 186/186. XML round trip and rejection; the 11 factory presets band by band; validity; loading sets exactly the preset and frees other bands, with matched gestures and no clicks; user presets in a temporary folder (save, overwrite, sort, load, delete, file names); next/previous; session restore of the name and modified state; top-bar wiring. pluginval strictness 5 (VST3, AU) and auval pass.
- Checked: the real preset folder stays empty (it was created by Logic on 2026-09-27, before M6b).
- 2026-09-28: owner tested in Logic. M6b marked done.
- Next step: M7 (dynamic EQ).
- Done:
ChannelMode, band_channel parameters; frame-wise routing inEqBand(Left/Right one channel, Mid/Side encode-filter-decode), crossfaded mode changes; complexresponse()on sections;StereoTransfer(2x2 chain matrices) for Auto Gain and the split sum curves; L/R/M/S node badges; channel menu in the panel and a Channel submenu in the node menu. - Tests added / passing: 172/172. Left/Right leave the other channel bit-exact; Mid/Side leave the other part bit-exact; neutral M/S returns the input; crossfades; matrices, chain order and power gain; Auto Gain exact per mode; K-weighted loudness of uncorrelated pink noise with a mix of modes and Auto Gain on: 0.018 dB change (limit 0.3); sum curves exact for L/R-only and M/S-only chains; measured L and R responses within 0.1 dB of the displayed sums. pluginval strictness 5 (VST3, AU) and auval pass.
- Test fixes: the M/S test relied on AudioProcessor::reset() clearing the filters, which the processor does not override; a 3 dB threshold was only the large-gain limit, replaced by the exact formula.
- Open issues: the processor does not override reset() (hosts call it on transport jumps; filter tails then continue). Candidate for a later small fix.
- 2026-09-28: owner tested in Logic. M6 marked done. Owner feedback: pre and post analyzer curves are both grey and hard to tell apart (to fix before M7).
- Analyzer colours fixed (owner's choice: different colours): pre muted blue, post warm light grey; a test keeps them apart. Where a band's fill overlaps the analyzer the tints mix with the band colour (bands are drawn on top).
- Next step: M7 (dynamic EQ).
- Done:
AnalyzerFifo(src/dsp),SpectrumAnalyzer,LevelMeter,AnalyzerSettings(src/ui); processor taps (pre/post, stereo, only while an editor is open); analyzer drawn under the curves (pre faint, post brighter) with its own dB scale; stereo output meter at the right edge (click resets the clip lights); bottom-bar controls for mode, resolution (shown as FFT size), speed, range and freeze, with tooltips; settings saved with the session. - Tests added / passing: 157/157. FIFO order, overflow and a two-thread stress test; 0 dBFS calibration at all four FFT sizes; peak location; equal levels across frequency; release rate; freeze; tilt; meter peak/RMS, hold, fall, clip latch; taps equal input/output exactly; no allocation in push or processBlock with the taps on; settings persistence. pluginval strictness 5 (VST3, AU) and auval pass.
- Deviation from the plan: no white-noise flatness test. With max-per-point binning noise cannot read flat (wide high-frequency points pick higher maxima); replaced by equal-amplitude sines across the range.
- Found: the FIFO stress test hung against the stub (now bounded by a deadline); peak hold ran a step long from float rounding (1 - 10 x 0.1); resolution and speed menus both read "Medium" (resolution now shows FFT size).
- Open issues: low frequencies look jagged at small FFT sizes (few bins per point there; higher resolution helps).
- Validation timing: pluginval VST3 3 s, AU 4 s, auval < 1 s. An earlier 10+ minute run most likely came from
restarting AudioComponentRegistrar before auval (full AU registry rescan; not proven). Restart it only when a
fresh AU build is missing from
auval -a, as CLAUDE.md says. - 2026-09-28: owner tested in Logic. M5 marked done.
- Next step: M6 (per-band stereo: Stereo / Left / Right / Mid / Side).
- Done:
NodeLayout,SelectionModel,NodeDragController,BandParameterWriterinsrc/ui/;ResponseDisplaynodes, selection (click, Cmd-click, area with Shift to add), drags (multi-band, Shift fine), Q by wheel/pinch, double-click add (Bell) / disable, right-click type/slope/disable menu, Delete/Backspace, all-bands-in-use notice, readout. Band tabs removed; the panel follows the primary selection and hides without one.EDITOR_WANTS_KEYBOARD_FOCUSon; unhandled keys return to the host. - Tests added / passing: 124/124. Interaction is tested through handlePress/Drag/Release/Wheel/Magnify/Key (no synthetic mouse events); a parameter listener checks every edit is inside matched host gestures. pluginval strictness 5 (VST3, AU) and auval pass.
- Modifier choice: Cmd-click toggles selection; Shift is fine-drag on a node and add-to-selection on an area (Shift could not also toggle on click without clashing with fine-drag).
- Open issues: a node under the band panel cannot be clicked there (reachable by area selection); host key handling (Delete reaching the plugin, space bar still reaching Logic) needs the owner's check.
- Owner check 1: disable and delete were the same (one flag meant both "exists" and "on"). Fixed with three band states (free / enabled / disabled), hidden band_used properties, state version 3 with migration. Disabled bands are grey and not editable; double-click on a node toggles; only Delete (key or menu) frees a band. 135/135 tests, pluginval and auval pass.
- 2026-09-28: owner re-check in Logic, everything tested (band states, keyboard, dragging, automation). M4 marked done.
- Next step: M5 (spectrum analyzer).
- Done: look-and-feel decision recorded and CLAUDE.md rule updated (overall layout may follow the reference;
components, styling and colour values our own).
FrequencyAxis,ResponseCurves(512 log points, recompute only on change),ResponseDisplay(grid, labels, dB scale, filled band curves, amber sum, clipping, range button 3/6/12/30 dB saved as thedisplayRangeDbstate property),BandPanel(16 coloured tabs, one band's controls, re-attached per band),TopBar,BottomBar. Editor 1280x770 default, 960x580-2400x1440. M2'sBandStrip/OutputStripremoved; menu-label logic moved toMenuLabels.h. - Tests added / passing: 104/104. Axis mapping and grid; band curves equal BandDesign; displayed sum vs the measured plugin response within 0.1 dB (44.1/48/96 kHz); no recompute without a change; range persistence and fallback; layout at three sizes; tab re-attachment (and no writes to the previous band); menus readable; greying; curve clipping. pluginval strictness 5 (VST3, AU) and auval pass.
- Checked by eye via snapshots: clipped "+12" scale label found and fixed.
- Owner review: layout works well. Band colours changed to a violet-to-red spectrum (390-645 nm; 700 nm made bands 14-16 identical, found in the snapshot, now covered by a distinctness test); tab numbers tinted for contrast. Text scaling left as is (owner's call). 108/108 tests, pluginval passes.
- 2026-09-28: owner reviewed the layout and spectrum colours. M3 marked done.
- Next step: M4 (interactive display).
- Done:
BandStrip,OutputStrip,CompactLookAndFeelinsrc/ui/; resizable one-row editor with a 15 Hz timer for greying and the Auto Gain readout; short menu labels when full ones do not fit; gain text never shows -0.00. Hidden[.snapshot]test renders the editor to PNG (used to check the layout by eye). - Tests added / passing: 89/89 (Debug), 99346/99346 assertions (Release). Editor structure, size limits, layout at min/default/max, all 99 controls attached both ways, menu choices, greying per type, readout, menu text shown and fitting at all sizes, repeated open/close. pluginval strictness 5 (VST3, AU, including its editor tests) and auval pass.
- CPU, 16 Brickwall bands at 96 kHz stereo, 1 s of audio: Debug 232 ms (4.3x real time), Release ~37 ms (~27x real time).
- Found (from the rendered snapshot, not the first tests): gain showing -0.00; menus truncated to "..." by
the V4 combo-box layout; then blank menus, because
ComboBox::getSelectedId()returns 0 afterchangeItemText. The first fit test passed on blank menus; it now requires the shown text to equal the selected item. In Release, the allocation counter's self-test was optimised away (elided new/delete); it now calls::operator newdirectly. - 2026-09-28: owner listening check in Logic, no clicks. M2 marked done.
- Next step: M3 (response curve display).
- Done:
output_gain(-30..+30 dB, continuous),auto_gain,output_invert(hint 2, no state bump).KWeighting(BS.1770-5 Tables 1-2, read from the standard's PDF),AutoGain,AutoGainUpdaterthread, output stage with one smoothed gain.Parameters::toBandSettingsshared by processor and thread. - Tests added / passing: 79/79. K-weighting +0.691 dB at 997 Hz (the standard's note 1); Auto Gain vs an independent 8192-point integration within 0.05 dB; measured K-weighted loudness of the whole plugin vs the model: 4.5e-7 dB (tone bands only) and 9e-8 dB (with an excluded cut); bit-exact invert and bypass; click-free gain and polarity changes; offset published with no processBlock calls; zero allocations with Auto Gain on. pluginval strictness 5 (VST3, AU) and auval pass.
- Found: a 0.01 dB snapping interval turns 0 dB into -6.7e-7 dB in float; output_gain made continuous. Band gains keep the M1 interval (a "0 dB" bell is -6.7e-7 dB; inaudible, not changed). Two test mistakes fixed: waiting for any non-zero offset raced the thread; the expectation with an excluded cut assumed K-weighted power is separable, which it is not where tone bands and the cut overlap.
- Next step: stage 5, grid editor for 16 bands plus output controls, then M2 validation.
- Done: 96 band parameters (
band<n>_{freq,gain,q,type,slope,enabled}, n = 1-16) viaParameters::id(), per-type preset defaults (all disabled), version hints 1 for M1's band1 freq/gain/q and 2 for the rest. Processor runs 16EqBands in series. State version 2 with migration of M1 sessions (band 1 = enabled bell). Idle bypassed bands skip their sample loop. - Tests added / passing: 64/64. Parameters (IDs, ranges, choices, defaults, hints), round trip of all 96, v1 migration from an M1-format blob, newer/invalid state ignored, bands in series equal the sum of their designs (within 0.1 dB at 44.1/48/96 kHz), bit-exact new instance, zero allocations with 16 active bands. CPU (Debug): 16 Brickwall bands at 96 kHz stereo, 1 s of audio in 232 ms (4.3x real time). pluginval strictness 5 (VST3, AU) and auval pass.
- Found: the first allocation test counted 90 allocations that came from the test's own
Parameters::id()strings, not fromprocessBlock(0). Fixed the test to look parameters up first, as a host does. - Open issues: the editor still shows only band 1 freq/gain/q, and band 1 now starts disabled, so a listening check needs the host's generic parameter view (Logic: Controls) until the stage-5 grid editor. Planned commits "16-band parameters" and "run 16 bands in the processor" landed as one.
- Next step: stage 4, output gain, static Auto Gain, output phase invert.
- Done:
FilterType(order fixed, saved in sessions) andCutSlope(index 0-15 = orders 1-16, 16 = Brickwall);BandSettings;BandDesign(settings -> sections, disabled = none, frequency capped at 0.49 fs);CascadeProcessor(own TDF-II cascade in double, fixed arrays; matchesjuce::dsp::IIR::Filter<double>within 1e-12 on random cascades);EqBand(two slots, 20 ms smoothing, 20 ms linear crossfade for type/slope/enable, later discrete requests wait and the latest wins). Processor runs band 1 throughEqBandas a bell.PeakingBandremoved. - Tests added / passing: 57/57. Measured response vs design within 0.1 dB for every type and cut slope
(2148 checks); sweeps for every type incl. Brickwall, largest step 0.142 (clean +12 dB sine: 0.1425);
crossfades 0.142 / 0.130 (enable); stress test settles on the last request; zero allocations in
EqBand::processandprocessBlock(global allocation counter inRealtimeAllocationTest). pluginval strictness 5 (VST3, AU) and auval pass. - Open issues: the measured-response test takes ~9 s in Debug. Pushing happens from the owner's IDE (this shell has no GitHub credentials); stage 1 is on GitHub.
- Next step: stage 3, 16 bands, parameters, state version 2 with v1 migration.
- Done: design classes for every shape in
src/dsp/: matched lowpass/highpass/bandpass (Vicanek 2016), two-pole Butterworth shelves (Vicanek 2024/25), one-pole shelf and tilt (Vicanek 2019), notch, all pass and first-order cut sections (own derivations),ButterworthCascade(orders 1-32),FlatTiltDesign,SectionCascade, sharedMatchedDesignMath. Bell moved onto the shared helpers. - Prototyped in Python first (scratch only) to measure deviations before setting bounds. Two prototype bugs found and fixed before any C++: odd-order Butterworth pole angles, and matching the first-order section at Nyquist (now matched at fc, which makes every cascade exact at the cutoff).
- Band pass: eq. 40 as printed cancels catastrophically at 20 Hz, Q 18, 192 kHz (B1 < 0 in double); replaced by an exact rearrangement. The bell's eq. 45 has the same shape (R1 - R2 phi1 - B0); it passes all tests, not changed.
- Tests added / passing: 39/39. C++ worst cases equal the Python prototype to 3 decimals.
- Open issues: FilterType/CutSlope enums moved to stage 2 (first used there).
- Next step: stage 2, EqBand with cascades and crossfaded type/slope/enable switching.
- Done: matched peaking design (Vicanek 2016, section 4.4) in
src/dsp/MatchedPeakingDesign;PeakingBand(JUCEIIR::Filter<double>per channel, in-place coefficient updates, 20 ms smoothing, 32-sample sub-blocks while ramping);band1_freq/gain/qin an APVTS; state as XML withstateVersion=1; rotary-knob editor. Repo now on GitHub (CatastrophicCoder/eq). - Finding: the matched bell is exact at DC and f0 but departs from analog between f0 and Nyquist, worst 0.317 dB at +1 octave (-18 dB, Q 0.5, f0/fs ~ 0.11); RBJ is 1.539 dB off on the same case. Owner chose the two-level check (see Decisions).
- Tests added / passing: 19/19. Design (f0 vs analog, ±1 octave bound, DC, extremum at f0, stability, 0 dB identity); band (measured impulse response vs design, sweep 20 Hz-20 kHz, ramp, reset); state (IDs/ranges/defaults, round trip, version, invalid data); processor (+12 dB applied end to end). Sweep largest step 0.139 / 0.128 / 0.064 at 44.1 / 48 / 96 kHz (limit 0.2). pluginval strictness 5 passes (VST3, AU); AU log warns "Current program is -1" (not a failure). auval passes.
- Open issues: the measured-response test takes ~4 s in Debug.
- 2026-09-28: owner listening check in Logic, frequency sweep with gain, no clicks. M1 marked done.
- Next step: M2 (full band set).
- Done: open decisions settled and recorded; docs moved to
docs/; repo-local git identity; branch renamed tomain;.gitignore, AGPLv3LICENSE; JUCE 9.0.2 and Catch2 v3.16.0 as submodules; CMake plugin skeleton (pass-through stereo processor, empty editor, AU/VST3/Standalone, auto-copy to~/Library/Audio/Plug-Ins). - Toolchain: CMake 4.4.3, Ninja 1.13.2, Apple clang 17 with the Command Line Tools only (no Xcode); AU builds fine without it.
- Tests added / passing:
ProcessorSmokeTest(stereo layout; bit-exact pass-through at 44.1/48/96 kHz), 2/2 pass. pluginval 1.0.4 strictness 5 passes for VST3 and AU;auval -v aufx Peq1 Ctcdpasses. - Owner checks: CLion build and AU load in Logic work. M0 marked done.
- Open issues: no git remote yet.
- Next step: M1 (matched peaking band).
- Done:
- Tests added / passing:
- Open issues:
- Next step: