mirror of
https://github.com/got-feedBack/feedBack-desktop.git
synced 2026-08-11 03:09:56 +00:00
2d0dd12abef8f5a107f40dd00f236e65869b0d7f
7
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2d0dd12abe |
fix(audio): input-callback double registration, stale-format race, effects schema alignment (#86)
* fix(audio): prefer same-backend duplex routing * fix(audio): centre mono input; limit duplex to same-endpoint devices Two issues found while testing the USB-guitar-cable path on Windows: 1. Centre a mono input. SourceChain::processBlock fell into the pass-through branch for a 1-channel input, filling only min(inputChannels, outputChannels) = 1 output channel and zeroing the rest, so a mono USB guitar cable played out of the left speaker only. A single-channel input is now broadcast across every output channel. 2. Only attempt the combined (duplex) device when input and output are the SAME physical endpoint. Two different endpoints of the same backend (USB cable in + separate speakers out) are independent hardware clocks; routing them through one duplex device was unstable across the app lifecycle (no audio until an explicit Apply, then distortion / dropouts / silent-in-song on navigation). Different endpoints now use the split path, whose ring bridges the two clocks. Same-endpoint duplex (one interface for in and out) keeps the low-latency win. Low latency for the two-device case is a follow-up that needs the device-lifecycle work (startup restore + reconfigure on navigation). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JEoFeTPSnz4NpwwCG52hnu * fix(audio): probe same-endpoint duplex the same way apply routes it Startup auto-apply (renderer init) fail-closes on probeDeviceOptionsDual's `compatible` verdict, but the probe still measured a COMBINED duplex device for any same-backend pair while setAudioDevices now opens split for different endpoints. That mismatch made the startup probe describe a config that isn't the one applied — surfacing as "no audio until I press Apply" for a USB cable + separate speakers. Gate the probe's duplex path on the same sameEndpointIntent (same type AND same device) the apply path uses, so a two-device pair is probed via the split path it will actually run on. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JEoFeTPSnz4NpwwCG52hnu * fix(audio): never feed chain processors blocks larger than prepared size WASAPI shared mode can deliver oversized blocks right after a device start. The NAM core pre-allocates its conv ring/output buffers to the Reset() maxBufferSize and only asserts (release no-op) on larger blocks; one oversized block corrupts the conv ring state and garbles all subsequent audio until the next Reset() — the 'first start heavily distorted until tone reset / engine restart' bug. - NAMProcessor::processBlock: process in slices of at most the prepared block size. - SignalChain::process: slice oversized device blocks into prepared-size chunks before any slot (VST/NAM/IR) sees them. See docs/audio-distortion-first-start-investigation.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(audio): close stale-format race when processors are added mid-reconfigure addProcessor/replaceProcessor prepare the incoming processor off the audio lock on N-API worker threads. A concurrent device reconfigure's SignalChain::prepare() can't see that processor (not slotted yet), so a slot could go live prepared at a stale sample rate / block size and stay wrong until the next device restart — heard as pitch-shifted/garbled monitoring when a chain loads while the device is being (re)opened (widest window: WASAPI exclusive mode's slower open). Re-check the chain's current format under the lock at insert/swap time and re-prepare if it moved; log the transition to stderr so tester logs show when the race fired. prepare() now publishes the format under the lock so the check can't tear. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(effects): align executor plan schema with rebranded capability layer The rebrand left a three-way schema split: rig_builder sent the old 'slopsmith.audio_effects.chain_plan.v1', the renderer capability layer validated against the new 'feedBack.…' id (rejecting every plan), and this executor still expected the old one. Result: every song chain load fell back to legacy clearChain+loadPreset — a full multi-VST rebuild per currentSong poll cycle, heard as continuous distortion during playback (tester logs: 4-6 rebuilds/session, slot IDs into the 90s). Executor now uses the rebranded id and accepts the legacy one as an alias (matching the capability layer's new alias), so neither side of the handoff can break on old plugin bundles. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(audio): guard input callback against double registration Tester main-process log showed two consecutive 'startAudio: duplex=0' lines: a transient audioDeviceStopped() (WASAPI exclusive opens fire one mid-start) cleared audioRunning while the input callback stayed attached, so the second startAudio() re-added it. JUCE then dispatched the input callback twice per block: DSP ran twice and each block was pushed into the split ring twice — every sample played twice (half speed, one octave down, garbled). stopAudio()'s single removeAudioCallback left the duplicate registration alive, wedging the engine (restart no longer helped) and keeping the exclusive-mode device open even after app close. Mirror the existing outputCallbackRegistered guard for the input side. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(audio): diagnostic instrumentation for tester repro builds Main-process stderr logging on every open lead, RT paths rate-limited (first-25 per anomaly + ~5s heartbeats per callback clock): - primary callback re-entrancy (duplicate registration detector) - oversized blocks on primary/output callbacks, SignalChain slicing, NAM chunking (pre-fix corruption trigger visibility) - ring fill + under/overflow counters (split-mode pacing) - device lifecycle: aboutToStart/stopped on both managers with sr/bs and callback-registration flags; startAudio guard-skip; stopAudio state - SourceChain.prepare format trace (stale-rate lead) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(audio): drop diagnostic heartbeats, keep anomaly detectors The 5s ring/format heartbeats served the distortion hunt and are noise now. Keep the cheap anomaly-only diagnostics (callback re-entrancy, oversized-block detectors, chain slicing/chunking, stale-format re-prepare, device lifecycle) — they log only on misbehavior and stay relevant for the exclusive-mode playback work. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: keep investigation notes out of the PR (local working notes) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(audio): gate lifecycle [diag] logs behind SLOPSMITH_SANDBOX_DEBUG Review follow-up (#86): the six lifecycle diagnostics (stopAudio, audioDeviceAboutToStart/Stopped, audioOutputAboutToStart/Stopped, SourceChain::prepare) printed unconditionally while the PR body claimed they were verbose-gated. Gate them behind the existing slopsmith_vst_trace::isEnabled() runtime flag (SLOPSMITH_SANDBOX_DEBUG — already flipped by the app's debug-logging switch, so tester debug runs still capture them). The RT-path anomaly detectors (primary re-entry, oversized-block) keep their firstN/anomaly bounds unchanged, as reviewed. VSTTrace.h now defines NOMINMAX/WIN32_LEAN_AND_MEAN before windows.h so including it from engine TUs doesn't clobber std::min. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: ChrisBeWithYou <chris@rifflarr.local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
3e3f1f868c |
fix(audio): never feed chain processors blocks larger than prepared size (#85)
* fix(audio): never feed chain processors blocks larger than prepared size WASAPI shared mode can deliver oversized blocks right after a device start. The NAM core pre-allocates its conv ring/output buffers to the Reset() maxBufferSize and only asserts (release no-op) on larger blocks; one oversized block corrupts the conv ring state and garbles all subsequent audio until the next Reset() — the 'first start heavily distorted until tone reset / engine restart' bug. - NAMProcessor::processBlock: process in slices of at most the prepared block size. - SignalChain::process: slice oversized device blocks into prepared-size chunks before any slot (VST/NAM/IR) sees them. See docs/audio-distortion-first-start-investigation.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(audio): prefer same-backend duplex routing * fix(audio): centre mono input; limit duplex to same-endpoint devices Two issues found while testing the USB-guitar-cable path on Windows: 1. Centre a mono input. SourceChain::processBlock fell into the pass-through branch for a 1-channel input, filling only min(inputChannels, outputChannels) = 1 output channel and zeroing the rest, so a mono USB guitar cable played out of the left speaker only. A single-channel input is now broadcast across every output channel. 2. Only attempt the combined (duplex) device when input and output are the SAME physical endpoint. Two different endpoints of the same backend (USB cable in + separate speakers out) are independent hardware clocks; routing them through one duplex device was unstable across the app lifecycle (no audio until an explicit Apply, then distortion / dropouts / silent-in-song on navigation). Different endpoints now use the split path, whose ring bridges the two clocks. Same-endpoint duplex (one interface for in and out) keeps the low-latency win. Low latency for the two-device case is a follow-up that needs the device-lifecycle work (startup restore + reconfigure on navigation). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JEoFeTPSnz4NpwwCG52hnu * fix(audio): probe same-endpoint duplex the same way apply routes it Startup auto-apply (renderer init) fail-closes on probeDeviceOptionsDual's `compatible` verdict, but the probe still measured a COMBINED duplex device for any same-backend pair while setAudioDevices now opens split for different endpoints. That mismatch made the startup probe describe a config that isn't the one applied — surfacing as "no audio until I press Apply" for a USB cable + separate speakers. Gate the probe's duplex path on the same sameEndpointIntent (same type AND same device) the apply path uses, so a two-device pair is probed via the split path it will actually run on. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JEoFeTPSnz4NpwwCG52hnu * fix(audio): close stale-format race when processors are added mid-reconfigure addProcessor/replaceProcessor prepare the incoming processor off the audio lock on N-API worker threads. A concurrent device reconfigure's SignalChain::prepare() can't see that processor (not slotted yet), so a slot could go live prepared at a stale sample rate / block size and stay wrong until the next device restart — heard as pitch-shifted/garbled monitoring when a chain loads while the device is being (re)opened (widest window: WASAPI exclusive mode's slower open). Re-check the chain's current format under the lock at insert/swap time and re-prepare if it moved; log the transition to stderr so tester logs show when the race fired. prepare() now publishes the format under the lock so the check can't tear. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: ChrisBeWithYou <chris@rifflarr.local> |
||
|
|
56e929da4e |
audio: replaceIR(slotId, path, gain) for in-place cab/IR swap (#83)
* audio: add replaceIR(slotId, path, gain) for in-place cab/IR swap Swap an existing convolution slot's IR without a full loadPreset, so the rest of the chain — the amp VST above all — is not torn down and rebuilt (that teardown is the ~1-2 s wait when changing cabs / mic position). Mirrors the existing loadIR worker but calls SignalChain::replaceProcessor(slotId, ...); optional gain updates the slot post-gain (the cab makeup). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * audio: expose replaceIR over the IPC bridge Wire the native replaceIR(slotId, path, gain) through audio-bridge (ipcMain handle) + preload, so renderers get feedBackDesktop.audio.replaceIR. Lets the rig-builder cab room swap a cab's IRs in place instead of a full loadPreset + param re-apply (that re-apply was the brief 'can't move the mic yet' lag). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * audio: replaceIR updates slot name/path so getChainState reflects the swap replaceProcessor deliberately preserves the target slot's name/path during the prepare/fault window (so a fault in prepareToPlay is blocklisted against the right plugin path). It kept them even on success, so after a successful replaceIR the slot's audio was the new IR but getChainState()/preset-save still reported the OLD IR name+path — a footgun for any consumer that persists a chain read back from getChainState(). Add optional newName/newPath to replaceProcessor, applied under the swap lock ONLY on success (empty = keep, so the sandbox-promotion caller is unchanged). ReplaceIRWorker passes "IR: <name>" + the new path, mirroring LoadIRWorker. Built (npm run build:audio) clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Jafz2001 <ignacio.fritis@mundotelecomunicaciones.cl> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: byrongamatos <xasiklas@gmail.com> |
||
|
|
c8415113c8 |
audio: per-slot postGain for parallel-branch loudness leveling (#58)
Adds an optional per-slot output gain (ProcessorSlot.postGain, default 1.0 = no-op) applied in SignalChain::runSlot after processBlock + pan, plumbed through setPostGain / IPC / preload / loadPreset / getChainState (mirroring setPan). Gives each parallel branch its own loudness trim. Review hardening (multi-angle + Codex): savePreset() now serializes postGain (it was read back but never written → save/load dropped it); setPostGain() rejects non-finite input (NaN would poison the buffer); new signalchain_postgain_test.cpp asserts gain scaling, NaN rejection, and serialization. Verified locally: build:audio clean; signalchain_postgain_test 5/5 pass. (Org CI runners failed to start — infra/billing, unrelated to the change; changes are platform-neutral C++.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
27e8f56ad8 |
fix(audio): promote in-process VST3 to sandbox on editor-open (Windows crash) (#54)
Windows-only crash fix: opening an in-process VST3 editor faults via WndProc on the background message thread. OpenPluginEditor now promotes the slot to the out-of-process sandbox (state transferred via get/setStateInformation) via the new SignalChain::replaceProcessor, and opens the editor there. Review hardening (multi-angle + Codex): state capture runs under the audio lock + SEH guard (SignalChain::captureVstStateForPromotion) so it can't race processBlock or fault the app; the transient sandbox pin is undone on promotion failure (isCrashedPlugin/removeCrashedPlugin) so a healthy plugin isn't stranded; replaceProcessor stages type/name/path for correct blocklist attribution; shared prepareForPlayback helper. CI green incl. addon (windows-latest). Editor-open crash repro on a real Windows host still recommended as follow-up. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
7671385ba8 |
Audio: song loudness normalization, in-process VSTs (perf), and stereo routing (#24)
* audio: flush denormals in the RT path + normalize the backing track
Two realtime-audio fixes (engine only — no change to amp/effect DSP):
1. Denormal flush (FTZ/DAZ). The signal path is full of IIR state (NAM, cab
IRs, VST amp/EQ/comp chains); after each note that state decays toward zero
and lands in the denormal range, where each float op is 10-100x slower. That
produced sporadic CPU spikes -> buffer underruns heard as random "scratches"
plus frame stutter (worse with larger buffers, independent of song/tone).
Add a scoped juce::ScopedNoDenormals at the three RT entry points:
- AudioEngine::audioDeviceIOCallbackWithContext (whole callback)
- SignalChain::process (the plugin chain)
- the sandbox worker's plugin processBlock in src/vst-host/main.cpp
(VST3s run OUT-OF-PROCESS, so the host-side FTZ doesn't reach them)
Denormals are sub -300 dBFS, so this is inaudible — CPU only, no tone change.
2. Backing-track loudness normalizer (BackingLeveler.h). Brings each song's
backing to a consistent -12 LUFS so songs don't jump in level, applied in
renderBackingBlockLocked BEFORE the mixer's backing-volume fader (so the
fader still attenuates). Short-term BS.1770 K-weighted AGC (slow, no pumping)
+ a -1 dBFS brickwall limiter. RT-safe (no allocation in process()).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* audio: extend denormal flush to the split-output path + reuse chain MidiBuffer
Opt-1 low-risk RT tidy-ups (no DSP/tone change):
- ScopedNoDenormals in audioOutputCallback (the split-mode output clock that
renders the backing track + phase-vocoder + leveler) — the primary callback's
scope doesn't reach this separate output thread, leaving an IIR/decay path
unprotected (a remaining source of the periodic "scratches").
- SignalChain::process reuses one juce::MidiBuffer across slots instead of
copy-constructing it per slot per block (avoids RT-thread allocation).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* audio: per-slot pan + parallel branch routing (St-1 stereo, engine side)
Adds pan-only stereo to the signal chain so the node editor can place one amp
left and another right, pan effects, and let stereo plugins pass true L/R.
ProcessorSlot gains two fields:
- pan : -1..+1 constant-power, applied to that slot's output (0 = no-op)
- branch : 0 = trunk (serial), >=1 = a parallel branch id
SignalChain::process keeps a bit-identical serial fast path when no slot has a
branch. When branches exist it runs the trunk-pre slots in place, snapshots that
as the split source, processes each branch on its own pre-allocated scratch
buffer, pans it, sums the branches into a merge bus, then runs any trunk-post
slots on the merged signal. Scratch is sized in prepare() (never on the RT
thread); falls back to serial for a non-stereo / oversized block.
The dual-mono amp output + post-amp pan is what yields "amp A left, amp B right"
without touching NAM or amp DSP. Preset schema emits pan/branch only when
non-default (mono presets unchanged); N-API gains setPan/setBranch and
getChainState/loadPreset round-trip them.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* audio: per-branch source channel (St-2) — feed a split L/R into separate branches
Extends the parallel-branch model so a stereo-out gear (e.g. a stereo delay) can
send its L output to one branch and its R to another. ProcessorSlot gains
branchSrc (0 = both, 1 = L, 2 = R); when seeding a branch from the split source,
L-only / R-only mono-izes that channel into the branch. Read from any slot in the
branch. N-API setBranchSrc + getChainState/preset round-trip it. Default 0 keeps
existing routing identical.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* audio-bridge: expose setPan/setBranch/setBranchSrc to the renderer
The engine N-API gained the stereo routing setters (setPan/setBranch/
setBranchSrc) but the main-process IPC handlers + the preload bridge didn't
forward them, so window.slopsmithDesktop.audio.setPan was undefined and the
node editor's stereo controls no-op'd. Wire all three through audio:setPan /
setBranch / setBranchSrc.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* audio: run scanned VSTs in-process + forward params + cut RT stalls
Big CPU/latency win for chains with VST plugins, plus the missing parameter
path. The out-of-process sandbox exists to crash-isolate the SCAN of unknown
plugins; a plugin only reaches a chain after it scanned cleanly, so paying the
per-block IPC cost (N serial round-trips, memcpy, poll waits) for every block
of playback was pure overhead.
- shouldSandbox(): default VST3 playback to IN-PROCESS. The runtime crash
blocklist + launch sentinel still route a faulting plugin back through the
sandbox on its next load, so it self-heals; only genuinely crash-prone gear
keeps paying for isolation. Eliminates the IPC round-trips + the per-load
subprocess spawn that caused the load-time "scratches".
- SignalChain::clear(): detach slots under a brief lock, destroy them OFF the
lock. Sandbox teardown is slow; doing it under `lock` starved the RT
ScopedTryLock and dropped audio blocks on every chain reload.
- AudioChannel::popBlock(): bounded busy-spin on the write index before the
blocking poll() — a fast plugin's output lands within microseconds, so we
skip the syscall + doorbell wakeup latency; a slow plugin falls through to the
efficient wait (correctness + heavy-chain cost unchanged).
- SandboxedProcessor::setSandboxedParameter() + SignalChain::setParameter()
route param changes to a sandboxed plugin over the control pipe (kSetParameter)
— the JUCE getParameters() proxy layer isn't wired, so without this a
sandboxed plugin's knobs/preset never reached it and it played at defaults.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* audio: PR #24 review follow-ups — POSIX fault guard + routing/spin/leveler fixes
Follow-up fixes from review of PR #24.
== POSIX in-process plugin fault guard (the main one) ==
PR #24 makes scanned VSTs run in-process by default. invokePlugin()'s catch(...)
only catches a plugin fault on Windows (where /EHa maps the SEH access violation
to a C++ exception); on macOS/Linux a plugin SIGSEGV during playback took down
the whole app, breaking the fail-soft-audio + cross-platform guarantees.
Add a POSIX fault guard in SignalChain.cpp: install chained SIGSEGV/SIGBUS/
SIGFPE/SIGILL handlers; while a guarded plugin call is live on the current
thread (thread-local, initial-exec TLS so the handler stays async-signal-safe),
siglongjmp() back into invokePlugin() and take the SAME blocklist+leak+survive
path as Windows. Faults outside a guarded call chain to the previously-installed
handler (V8/ASan/default), so real crashes and sanitizers are never masked. The
guard's armed flag is restored on EVERY exit from the guarded region — normal
return, signal-fault longjmp, and a normal C++ exception from the plugin — so a
thread is never left armed with a stale landing pad. Known limit: stack-overflow
faults aren't reliably caught (no sigaltstack on JUCE audio threads).
Comments in SandboxFactory_shared.cpp updated to match the kept in-process
default (the stale 'every VST3 sandboxes' / 'diagnostic tagging only' notes).
== Smaller correctness/quality fixes ==
- SignalChain parallel path: a branch==0 (trunk) slot interleaved inside the
branch region was run by none of the loops -> silently dropped. Detect the
region first and fall back to a serial chain (jassertfalse in debug) so no
slot is lost if the node-editor contiguity invariant breaks.
- AudioChannel pop busy-spin: add a cpuRelax() (_mm_pause / arm yield) hint.
- BackingLeveler: reset AGC/limiter state on loadBackingTrack so a new song
doesn't inherit the previous track's gain follower and briefly mis-level.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test: integration test for the in-process plugin fault guard
Drives deliberately-faulting in-process AudioProcessors through a real
SignalChain::process() and asserts the host survives, the processor is released,
and it's added to the crash blocklist (shouldSandbox() then routes it
out-of-process). Covers BOTH fault kinds: a hardware SIGSEGV (POSIX guard /
Windows SEH) and a normal C++ exception (the path that must leave the guard
disarmed). End-to-end counterpart to the standalone mechanism check — exercises
the actual invokePlugin() guard.
Lives in the POSIX-only sandbox e2e harness (already links juce_audio_processors
+ the full sandbox set). Leak detection is disabled for the target because the
guard leaks the faulting processor by design.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Jafz2001 <ignacio.fritis@mundotelecomunicaciones.cl>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Byron Gamatos <xasiklas@gmail.com>
|
||
|
|
bd603184d5 | Clean release snapshot |