mirror of
https://github.com/got-feedBack/feedBack-desktop.git
synced 2026-08-13 12:20:02 +00:00
* 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>
140 lines
7.0 KiB
CMake
140 lines
7.0 KiB
CMake
# Standalone end-to-end harness for the out-of-process sandbox runtime.
|
|
# JUCE-only (no cmake-js / node-addon-api / ONNX): builds a passthrough VST3
|
|
# fixture, the real slopsmith-vst-host child, and a host-side driver that spawns
|
|
# the child, loads the plugin, and round-trips audio over the shm ring.
|
|
#
|
|
# Heavier than tests/sandbox/standalone (it pulls in juce_audio_processors +
|
|
# juce_gui_basics + a VST3), so it lives in its own bootstrap / CI job. POSIX
|
|
# only — the e2e driver uses the fd-passing host API.
|
|
#
|
|
# cmake -S tests/sandbox/e2e -B build/e2e -DCMAKE_BUILD_TYPE=Debug
|
|
# cmake --build build/e2e
|
|
# ctest --test-dir build/e2e --output-on-failure
|
|
cmake_minimum_required(VERSION 3.22)
|
|
project(slopsmith_sandbox_e2e VERSION 1.0.0 LANGUAGES C CXX)
|
|
|
|
set(CMAKE_CXX_STANDARD 20)
|
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|
|
|
get_filename_component(REPO_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../../.." ABSOLUTE)
|
|
if(NOT EXISTS "${REPO_ROOT}/JUCE/CMakeLists.txt")
|
|
message(FATAL_ERROR "JUCE submodule not found at ${REPO_ROOT}/JUCE. "
|
|
"Run: git submodule update --init --recursive")
|
|
endif()
|
|
if(WIN32)
|
|
message(FATAL_ERROR "The sandbox e2e harness is POSIX-only (fd-passing host API).")
|
|
endif()
|
|
add_subdirectory("${REPO_ROOT}/JUCE" juce_build)
|
|
|
|
set(SANDBOX "${REPO_ROOT}/src/audio/Sandbox")
|
|
enable_testing()
|
|
|
|
# --- passthrough VST3 fixture (doubles its input) ---
|
|
juce_add_plugin(SlopPassThrough
|
|
PRODUCT_NAME "SlopPassThrough"
|
|
COMPANY_NAME "Slop"
|
|
PLUGIN_MANUFACTURER_CODE Slop
|
|
PLUGIN_CODE Sptp
|
|
FORMATS VST3
|
|
IS_SYNTH FALSE
|
|
NEEDS_MIDI_INPUT FALSE
|
|
VST3_CATEGORIES Fx)
|
|
target_sources(SlopPassThrough PRIVATE passthrough.cpp)
|
|
target_compile_definitions(SlopPassThrough PRIVATE
|
|
JUCE_WEB_BROWSER=0 JUCE_USE_CURL=0 JUCE_VST3_CAN_REPLACE_VST2=0)
|
|
target_link_libraries(SlopPassThrough PRIVATE
|
|
juce::juce_audio_utils juce::juce_audio_processors juce::juce_gui_basics
|
|
juce::juce_audio_plugin_client)
|
|
|
|
# --- the real vst-host child (POSIX sources) ---
|
|
add_executable(slopsmith-vst-host
|
|
"${REPO_ROOT}/src/vst-host/main.cpp"
|
|
"${REPO_ROOT}/src/audio/VSTHost.cpp"
|
|
"${SANDBOX}/Protocol.cpp"
|
|
"${SANDBOX}/ControlChannel_shared.cpp" "${SANDBOX}/ControlChannel_posix.cpp"
|
|
"${SANDBOX}/AudioChannel_shared.cpp" "${SANDBOX}/AudioChannel_posix.cpp")
|
|
target_include_directories(slopsmith-vst-host PRIVATE "${REPO_ROOT}/src/audio")
|
|
target_link_libraries(slopsmith-vst-host PRIVATE
|
|
juce::juce_audio_basics juce::juce_audio_devices juce::juce_audio_formats
|
|
juce::juce_audio_processors juce::juce_core juce::juce_data_structures
|
|
juce::juce_dsp juce::juce_events juce::juce_graphics juce::juce_gui_basics)
|
|
target_compile_definitions(slopsmith-vst-host PRIVATE
|
|
JUCE_PLUGINHOST_VST3=1 JUCE_PLUGINHOST_AU=0 JUCE_PLUGINHOST_LV2=0
|
|
JUCE_WEB_BROWSER=0 JUCE_USE_CURL=0 JUCE_DISPLAY_SPLASH_SCREEN=0
|
|
JUCE_MODAL_LOOPS_PERMITTED=1 JUCE_STANDALONE_APPLICATION=1 JUCE_REPORT_APP_USAGE=0)
|
|
# main.cpp calls XInitThreads/XSetErrorHandler directly on Linux to install a
|
|
# non-fatal X error handler (JUCE only does this for standalone JUCEApplications,
|
|
# which this child is not). JUCE itself dlopen()s libX11, but our direct calls
|
|
# need it link-time. Mirrors src/vst-host/CMakeLists.txt.
|
|
if(UNIX AND NOT APPLE)
|
|
find_package(X11 REQUIRED)
|
|
target_link_libraries(slopsmith-vst-host PRIVATE ${X11_LIBRARIES})
|
|
target_include_directories(slopsmith-vst-host PRIVATE ${X11_INCLUDE_DIR})
|
|
endif()
|
|
|
|
# --- host-side e2e driver ---
|
|
add_executable(sandbox_e2e_test
|
|
e2e_test.cpp
|
|
"${SANDBOX}/SandboxedProcessor.cpp"
|
|
"${SANDBOX}/SandboxFactory_shared.cpp" "${SANDBOX}/SandboxFactory_posix.cpp"
|
|
"${SANDBOX}/AudioChannel_shared.cpp" "${SANDBOX}/AudioChannel_posix.cpp"
|
|
"${SANDBOX}/ControlChannel_shared.cpp" "${SANDBOX}/ControlChannel_posix.cpp"
|
|
"${SANDBOX}/SubprocessHandle_posix.cpp"
|
|
"${SANDBOX}/Protocol.cpp")
|
|
target_include_directories(sandbox_e2e_test PRIVATE "${REPO_ROOT}/src/audio")
|
|
target_link_libraries(sandbox_e2e_test PRIVATE
|
|
juce::juce_audio_basics juce::juce_audio_devices juce::juce_audio_formats
|
|
juce::juce_audio_processors juce::juce_core juce::juce_dsp juce::juce_events)
|
|
target_compile_definitions(sandbox_e2e_test PRIVATE
|
|
JUCE_PLUGINHOST_VST3=1 JUCE_WEB_BROWSER=0 JUCE_USE_CURL=0
|
|
JUCE_STANDALONE_APPLICATION=0 JUCE_REPORT_APP_USAGE=0)
|
|
add_dependencies(sandbox_e2e_test slopsmith-vst-host SlopPassThrough_VST3)
|
|
|
|
# The VST3 bundle lands in <build>/SlopPassThrough_artefacts/<config>/VST3/.
|
|
add_test(NAME sandbox_e2e_test
|
|
COMMAND sandbox_e2e_test
|
|
"$<TARGET_FILE:slopsmith-vst-host>"
|
|
"${CMAKE_BINARY_DIR}/SlopPassThrough_artefacts/$<CONFIG>/VST3/SlopPassThrough.vst3")
|
|
|
|
# --- in-process plugin fault-guard integration test ---
|
|
# Builds the REAL SignalChain against the full sandbox set and drives a
|
|
# deliberately-segfaulting in-process plugin through SignalChain::process();
|
|
# asserts invokePlugin()'s fault guard (POSIX siglongjmp / Windows SEH) keeps the
|
|
# host alive, releases the processor, and blocklists it. No subprocess / VST3
|
|
# fixture needed — the faulting plugin is an in-process AudioProcessor.
|
|
add_executable(signalchain_fault_test
|
|
signalchain_fault_test.cpp
|
|
"${REPO_ROOT}/src/audio/SignalChain.cpp"
|
|
"${SANDBOX}/SandboxedProcessor.cpp"
|
|
"${SANDBOX}/SandboxFactory_shared.cpp" "${SANDBOX}/SandboxFactory_posix.cpp"
|
|
"${SANDBOX}/AudioChannel_shared.cpp" "${SANDBOX}/AudioChannel_posix.cpp"
|
|
"${SANDBOX}/ControlChannel_shared.cpp" "${SANDBOX}/ControlChannel_posix.cpp"
|
|
"${SANDBOX}/SubprocessHandle_posix.cpp"
|
|
"${SANDBOX}/Protocol.cpp")
|
|
target_include_directories(signalchain_fault_test PRIVATE "${REPO_ROOT}/src/audio")
|
|
target_link_libraries(signalchain_fault_test PRIVATE
|
|
juce::juce_audio_basics juce::juce_audio_devices juce::juce_audio_formats
|
|
juce::juce_audio_processors juce::juce_core juce::juce_data_structures
|
|
juce::juce_dsp juce::juce_events)
|
|
target_compile_definitions(signalchain_fault_test PRIVATE
|
|
JUCE_PLUGINHOST_VST3=1 JUCE_WEB_BROWSER=0 JUCE_USE_CURL=0
|
|
JUCE_STANDALONE_APPLICATION=0 JUCE_REPORT_APP_USAGE=0
|
|
# The guard INTENTIONALLY leaks the faulting processor (destructing it on a
|
|
# corrupted heap is the very hazard it avoids), so JUCE's leak detector would
|
|
# fire a Debug assertion at shutdown for a by-design leak. Disable it here.
|
|
JUCE_CHECK_MEMORY_LEAKS=0)
|
|
add_test(NAME signalchain_fault_test COMMAND signalchain_fault_test)
|
|
|
|
# Orphan-cleanup regression (issue #265): host crash → child must not orphan.
|
|
# Linux-only — the driver's leak path + PR_SET_PDEATHSIG are JUCE_LINUX-gated;
|
|
# on macOS the env var is a no-op so this would assert clean-shutdown, not the
|
|
# crash path, which would be misleading. (UNIX AND NOT APPLE matches the X11
|
|
# linkage block above and excludes any other non-mac POSIX target.)
|
|
if(UNIX AND NOT APPLE)
|
|
add_test(NAME sandbox_e2e_leak
|
|
COMMAND bash "${CMAKE_CURRENT_SOURCE_DIR}/leak_test.sh"
|
|
"$<TARGET_FILE:sandbox_e2e_test>"
|
|
"$<TARGET_FILE:slopsmith-vst-host>"
|
|
"${CMAKE_BINARY_DIR}/SlopPassThrough_artefacts/$<CONFIG>/VST3/SlopPassThrough.vst3")
|
|
endif()
|