mirror of
https://github.com/got-feedBack/feedBack-desktop.git
synced 2026-08-18 06:22:13 +00:00
* feat(audio): streamer mix outputs — one stream bus to a 2nd output device (PR1) Built-in routing so a streamer can send a separate mix (game ± their guitar tone) to a second output device for OBS/Discord capture, while still monitoring locally — no VoiceMeeter/Reaper. PR1 of the design in docs/streamer-mix-outputs.md. Architecture: this inverts the engine's proven Phase-2 multi-INPUT-device pattern to the output side. A new StreamSink = its own AudioDeviceManager + drain callback + packed drop-oldest SPSC ring (a mirror of InputDeviceSlot). The PRODUCER is the main output path (both the duplex callback and the split audioOutputCallback): it snapshots the guitar monitor mix BEFORE backing is added, then composes the stream submix (includeGuitar ? guitar : 0) + (includeBacking ? backing : 0) × gain and packs it into the sink ring. The CONSUMER (streamSinkCallback) drains the ring to the second device. Backing is rendered once on the master clock and fanned to the stream ring (never re-advances the transport / touches backingLock). Default off → zero behaviour change; the sink reopens across restarts (reopenDesiredStreamSink, mirroring reopenDesiredExtraInputs). Surface: NodeAddon setStreamOutputDevice/clearStreamOutput/setStreamBus/ setStreamBusGain/getStreamSinkLevel/isStreamOutputActive/getStreamUnderflowCount → audio:* IPC → preload → a new "Streaming & Extra Outputs" section on the Audio page (device picker, game/guitar toggles, gain, a meter mirroring what OBS/Discord receives; persisted to localStorage). v1 rejects a sample-rate-mismatched sink with a clear error (async SRC is PR3). Scope (PR1): ONE stream bus = game ± the guitar monitor mix. Per-source A/B mixes (re-amped DI vs wet as separate OBS tracks) and per-bus mute that lets a local monitor-kill (#47) NOT silence the stream are PR2 (see the doc). No virtual driver shipped — route to a spare output / virtual cable / Go-Live capture. NOT compiled or run on the author's box — this is native C++ (AudioEngine / NodeAddon) that needs a desktop build. Renderer JS verified with node --check; TS bridge/preload are additive (AudioModule is an index type so the calls typecheck). Draft pending a build + a tester pass (see the PR checklist). Refs got-feedback/feedBack-desktop#48 (tracking), #46/#47 (audio-engine family). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF * fix(audio): harden streamer-mix sink lifecycle/RT-safety (review on PR #49) Addresses the P0/P1/P2/P3 findings from the Codex + manual review. P0 — shutdown UAF: ~AudioEngine() never tore down the stream sink, and StreamSink declared `manager` before `callback`/`ring`, so the manager could be destroyed after the callback/ring it drives. stopAudio() now closes the sink (and the dtor calls stopAudio()), and `manager` is declared LAST so it destructs first even if a teardown path is missed. P1 — stopAudio() ignored the sink: the 2nd output device kept running and underflowing while "stopped". It now closes via closeStreamSinkDevice() (intent preserved → startAudio() reopens, like extra inputs). P1 — split-path producer buffers could realloc under a live callback: streamGuitarScratch/streamMixScratch are now sized to a fixed capacity (>= the ring) so a same/smaller-block device restart on either clock never reallocates them mid-use. P1 — split path read backingBuffer OUTSIDE backingLock (duplex held it): composeAndPushStreamMix in audioOutputCallback now runs inside the lock scope, so backingBuffer is read under the lock that guards its resize. P1 — live setStreamOutputDevice() broke the SPSC single-writer invariant: streamSinkAboutToStart() resets the ring while the producer might still be writing. It now clears `active` before reconfiguring so the producer stops, and only re-arms after a clean open. P1 — failed open left stale state: a shared `fail()` path now closes the device and drops the desired intent, so a deterministic failure (e.g. SR mismatch) isn't retried every start and never reports active with no device. The renderer keeps its own persisted choice. P2 — streamSinkStopped() was empty: now marks the sink inactive (and clears the meter) on an unplanned device loss, preserving intent. P2 — no ring-capacity guard on the duplex path: composeAndPushStreamMix skips (and counts) a block larger than the ring instead of wrapping. P2 — gain NaN/Inf + bridge bool coercion: native sanitizeStreamGain() (finite, clamped 0..8); the TS bridge requires real booleans (no Boolean("false")===true) and a finite gain. P3 — drop-oldest now counted via streamSink.overflowCount, exposed as getStreamOverflowCount() through the addon/bridge/preload (mirrors underflow) for drift diagnosis. Still NOT compiled here (needs a desktop build). TS typechecks clean (tsc --noEmit); renderer node --check clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(audio): make stream scratch fixed-capacity; document reconfig tail Follow-up to the review-fix commit, closing the two residual edge cases from the Codex re-review: - Producer scratch (streamGuitarScratch/streamMixScratch) is now sized to a FIXED capacity == the ring and never grown with the block size. Oversized blocks are already skipped by the capacity guard, so a fixed cap is sufficient and means the buffers allocate exactly once — they can never realloc under a live split-mode producer for ANY later/hotplug block size (previously a larger restart block could still realloc). - Reworded the setStreamOutputDevice() comment to stop overstating the active=false barrier: it prevents NEW producer pushes, but a block already in flight can finish one push before the (much slower) device reopen drives streamSinkAboutToStart's ring reset. Net worst case is one imperfect block on the stream bus (never the local monitor) during a manual device switch — atomic, no data race, no UAF. Documented as a known PR1 limitation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(audio): count oversized stream blocks (capacity guard before scratch guard) Codex re-review nit: with the fixed-size scratch (== ring), an oversized block tripped the undersized-scratch guard first and was dropped without being counted. Check the ring-capacity guard FIRST so oversized duplex blocks are always counted as stream overflows; keep the scratch guard after it as cold-start/reconfig defense. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: ChrisBeWithYou <chris@rifflarr.local> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: byrongamatos <xasiklas@gmail.com>
301 lines
16 KiB
HTML
301 lines
16 KiB
HTML
<div id="audio-engine-panel" class="p-4 pt-20 max-w-5xl mx-auto">
|
||
<!-- Status Bar -->
|
||
<div id="ae-status" class="mb-4 p-3 rounded-lg bg-slate-800/50 flex items-center justify-between">
|
||
<div class="flex items-center gap-3">
|
||
<span id="ae-status-dot" class="w-3 h-3 rounded-full bg-red-500"></span>
|
||
<span id="ae-status-text" class="text-sm text-slate-400">Audio engine not available</span>
|
||
</div>
|
||
<div class="flex items-center gap-2">
|
||
<span id="ae-latency" class="text-xs text-slate-500"></span>
|
||
<button id="ae-toggle" class="px-3 py-1 rounded bg-emerald-600 hover:bg-emerald-500 text-sm font-medium disabled:opacity-50" disabled>
|
||
Start
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Device Settings -->
|
||
<div class="mb-6">
|
||
<h3 class="text-lg font-semibold mb-3 text-slate-200">Audio Device</h3>
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
<div>
|
||
<label class="block text-xs text-slate-400 mb-1">Input Device Type</label>
|
||
<select id="ae-device-type" class="w-full bg-slate-700 border border-slate-600 rounded px-3 py-2 text-sm">
|
||
<option>Loading...</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label class="block text-xs text-slate-400 mb-1">Output Device Type</label>
|
||
<select id="ae-output-device-type" class="w-full bg-slate-700 border border-slate-600 rounded px-3 py-2 text-sm">
|
||
<option>Loading...</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label class="block text-xs text-slate-400 mb-1">Input Channel</label>
|
||
<select id="ae-input-channel" class="w-full bg-slate-700 border border-slate-600 rounded px-3 py-2 text-sm">
|
||
<option value="-1">Default pair (Mono Mix)</option>
|
||
</select>
|
||
</div>
|
||
<div class="md:col-span-2">
|
||
<label class="flex items-center gap-2 cursor-pointer">
|
||
<input type="checkbox" id="ae-monitor-mute" checked
|
||
class="w-4 h-4 rounded bg-slate-700 border-slate-600 accent-emerald-500">
|
||
<span class="text-sm text-slate-300">Mute direct monitoring</span>
|
||
<span class="text-xs text-slate-500">(mutes the dry passthrough only — a loaded amp sim is still heard; use “Disable input monitoring” below to silence everything)</span>
|
||
</label>
|
||
</div>
|
||
<div class="md:col-span-2">
|
||
<label class="flex items-center gap-2 cursor-pointer">
|
||
<input type="checkbox" id="ae-monitor-kill"
|
||
class="w-4 h-4 rounded bg-slate-700 border-slate-600 accent-emerald-500">
|
||
<span class="text-sm text-slate-300">Disable input monitoring</span>
|
||
<span class="text-xs text-slate-500">(play through your own amp/rig — silences all in-app monitoring, including amp sims)</span>
|
||
</label>
|
||
</div>
|
||
<div class="md:col-span-2">
|
||
<label class="flex items-center gap-2 cursor-pointer">
|
||
<input type="checkbox" id="ae-amp-sims"
|
||
class="w-4 h-4 rounded bg-slate-700 border-slate-600 accent-emerald-500">
|
||
<span class="text-sm text-slate-300">Use in-app amp sims</span>
|
||
<span class="text-xs text-slate-500">(auto-load your saved tone chain for monitoring — leave off to play through your own rig)</span>
|
||
</label>
|
||
</div>
|
||
<div>
|
||
<label class="block text-xs text-slate-400 mb-1">Input Device</label>
|
||
<select id="ae-input-device" class="w-full bg-slate-700 border border-slate-600 rounded px-3 py-2 text-sm">
|
||
<option>Select device type first</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label class="block text-xs text-slate-400 mb-1">Output Device</label>
|
||
<select id="ae-output-device" class="w-full bg-slate-700 border border-slate-600 rounded px-3 py-2 text-sm">
|
||
<option>Select device type first</option>
|
||
</select>
|
||
</div>
|
||
<div id="ae-sr-mismatch-warning" role="alert" aria-live="polite" class="md:col-span-2 hidden p-2 rounded bg-amber-900/40 border border-amber-600/40 text-xs text-amber-200">
|
||
Input and output devices don't share a compatible sample rate. Pick devices that both support the same rate (typical: 48000 Hz) or use the same device for both directions.
|
||
</div>
|
||
<div>
|
||
<label class="block text-xs text-slate-400 mb-1">Sample Rate</label>
|
||
<select id="ae-sample-rate" class="w-full bg-slate-700 border border-slate-600 rounded px-3 py-2 text-sm">
|
||
<option value="44100">44100 Hz</option>
|
||
<option value="48000" selected>48000 Hz</option>
|
||
<option value="96000">96000 Hz</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label class="block text-xs text-slate-400 mb-1">Buffer Size</label>
|
||
<select id="ae-buffer-size" class="w-full bg-slate-700 border border-slate-600 rounded px-3 py-2 text-sm">
|
||
<option value="64">64 samples (~1.3ms)</option>
|
||
<option value="128">128 samples (~2.7ms)</option>
|
||
<option value="256" selected>256 samples (~5.3ms)</option>
|
||
<option value="512">512 samples (~10.7ms)</option>
|
||
<option value="1024">1024 samples (~21.3ms)</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<button id="ae-apply-device" class="mt-3 px-4 py-2 rounded bg-blue-600 hover:bg-blue-500 text-sm font-medium">
|
||
Apply Device Settings
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Level Meters -->
|
||
<div class="mb-6">
|
||
<h3 class="text-lg font-semibold mb-3 text-slate-200">Levels</h3>
|
||
<div class="grid grid-cols-2 gap-4">
|
||
<div>
|
||
<label class="block text-xs text-slate-400 mb-1">Input</label>
|
||
<div class="h-4 bg-slate-800 rounded overflow-hidden">
|
||
<div id="ae-meter-input" class="h-full bg-emerald-500 transition-all duration-75" style="width:0%"></div>
|
||
</div>
|
||
<div class="flex justify-between mt-1">
|
||
<input type="range" id="ae-input-gain" min="-60" max="12" step="0.1" value="0" class="w-full accent-emerald-500">
|
||
<span id="ae-input-gain-label" class="text-xs text-slate-400 ml-2 whitespace-nowrap">0.0 dB</span>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<label class="block text-xs text-slate-400 mb-1">Output</label>
|
||
<div class="h-4 bg-slate-800 rounded overflow-hidden">
|
||
<div id="ae-meter-output" class="h-full bg-blue-500 transition-all duration-75" style="width:0%"></div>
|
||
</div>
|
||
<div class="flex justify-between mt-1">
|
||
<input type="range" id="ae-output-gain" min="-60" max="12" step="0.1" value="0" class="w-full accent-blue-500">
|
||
<span id="ae-output-gain-label" class="text-xs text-slate-400 ml-2 whitespace-nowrap">0.0 dB</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Streaming & Extra Outputs (PR1: one stream bus → a second output device for
|
||
OBS/Discord capture, independent of your local monitor). Advanced; off by default. -->
|
||
<div class="mb-6">
|
||
<h3 class="text-lg font-semibold mb-3 text-slate-200">Streaming & Extra Outputs</h3>
|
||
<p class="text-xs text-slate-400 mb-3">
|
||
Send a separate mix (game ± your guitar tone) to a second output device that
|
||
OBS or Discord can capture — while you keep monitoring here. Pick a spare output
|
||
or a virtual cable; on Discord, share that device or use Go Live.
|
||
</p>
|
||
<label class="flex items-center gap-2 cursor-pointer mb-3">
|
||
<input type="checkbox" id="ae-stream-enable"
|
||
class="w-4 h-4 rounded bg-slate-700 border-slate-600 accent-fuchsia-500">
|
||
<span class="text-sm text-slate-300">Send a stream mix to a second output</span>
|
||
</label>
|
||
<div id="ae-stream-config" class="space-y-3" style="display: none;">
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||
<div>
|
||
<label class="block text-xs text-slate-400 mb-1">Stream Output Type</label>
|
||
<select id="ae-stream-type" class="w-full bg-slate-700 border border-slate-600 rounded px-3 py-2 text-sm"></select>
|
||
</div>
|
||
<div>
|
||
<label class="block text-xs text-slate-400 mb-1">Stream Output Device</label>
|
||
<select id="ae-stream-device" class="w-full bg-slate-700 border border-slate-600 rounded px-3 py-2 text-sm"></select>
|
||
</div>
|
||
</div>
|
||
<div class="flex flex-wrap gap-4">
|
||
<label class="flex items-center gap-2 cursor-pointer">
|
||
<input type="checkbox" id="ae-stream-game" checked
|
||
class="w-4 h-4 rounded bg-slate-700 border-slate-600 accent-fuchsia-500">
|
||
<span class="text-sm text-slate-300">Include game audio</span>
|
||
</label>
|
||
<label class="flex items-center gap-2 cursor-pointer">
|
||
<input type="checkbox" id="ae-stream-guitar" checked
|
||
class="w-4 h-4 rounded bg-slate-700 border-slate-600 accent-fuchsia-500">
|
||
<span class="text-sm text-slate-300">Include my guitar tone</span>
|
||
</label>
|
||
</div>
|
||
<div>
|
||
<label class="block text-xs text-slate-400 mb-1">Stream level</label>
|
||
<div class="h-4 bg-slate-800 rounded overflow-hidden">
|
||
<div id="ae-stream-meter" class="h-full bg-fuchsia-500 transition-all duration-75" style="width:0%"></div>
|
||
</div>
|
||
<div class="flex justify-between mt-1">
|
||
<input type="range" id="ae-stream-gain" min="-60" max="12" step="0.1" value="0" class="w-full accent-fuchsia-500">
|
||
<span id="ae-stream-gain-label" class="text-xs text-slate-400 ml-2 whitespace-nowrap">0.0 dB</span>
|
||
</div>
|
||
<p class="text-[11px] text-fuchsia-300/80 mt-1">This is what OBS/Discord will hear.</p>
|
||
</div>
|
||
<p id="ae-stream-status" class="text-xs text-slate-400"></p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Noise Gate (AmpliTube-style: threshold, release, depth → native setNoiseGate) -->
|
||
<div class="mb-6">
|
||
<h3 class="text-lg font-semibold mb-3 text-slate-200">Noise Gate</h3>
|
||
<label class="flex items-center gap-2 cursor-pointer">
|
||
<input type="checkbox" id="ae-noise-gate-enable"
|
||
class="w-4 h-4 rounded bg-slate-700 border-slate-600 accent-emerald-500">
|
||
<span class="text-sm text-slate-300">Enable noise gate</span>
|
||
</label>
|
||
<div id="ae-noise-gate-threshold-wrap" class="mt-3 space-y-3" style="display: none;">
|
||
<div>
|
||
<label class="block text-xs text-slate-400 mb-1" for="ae-noise-gate-threshold">
|
||
Gate Threshold (dBFS): <span id="ae-noise-gate-threshold-label">-60 dB</span>
|
||
</label>
|
||
<input type="range" id="ae-noise-gate-threshold" min="-96" max="0" step="1" value="-60"
|
||
class="w-full accent-emerald-500">
|
||
</div>
|
||
<div>
|
||
<label class="block text-xs text-slate-400 mb-1" for="ae-noise-gate-release">
|
||
Release: <span id="ae-noise-gate-release-label">100 ms</span>
|
||
</label>
|
||
<input type="range" id="ae-noise-gate-release" min="5" max="2000" step="5" value="100"
|
||
class="w-full accent-emerald-500">
|
||
</div>
|
||
<div>
|
||
<label class="block text-xs text-slate-400 mb-1" for="ae-noise-gate-depth">
|
||
Depth: <span id="ae-noise-gate-depth-label">-60 dB</span>
|
||
</label>
|
||
<input type="range" id="ae-noise-gate-depth" min="-100" max="0" step="1" value="-60"
|
||
class="w-full accent-emerald-500">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Tone Polish — fixed 3-band mastering EQ on the live guitar bus
|
||
(HPF 80 Hz, low shelf -3 dB @ 180 Hz, peak -0.5 dB @ 200 Hz Q=1).
|
||
Saved per chain preset; default on. -->
|
||
<div class="mb-6">
|
||
<h3 class="text-lg font-semibold mb-3 text-slate-200">Tone Polish</h3>
|
||
<label class="flex items-center gap-2 cursor-pointer">
|
||
<input type="checkbox" id="ae-tone-polish-enable" checked
|
||
class="w-4 h-4 rounded bg-slate-700 border-slate-600 accent-emerald-500">
|
||
<span class="text-sm text-slate-300">
|
||
Apply Tone Polish
|
||
</span>
|
||
</label>
|
||
<p class="mt-1 text-xs text-slate-500">
|
||
HPF 80 Hz · low shelf −3 dB @ 180 Hz · bell −0.5 dB @ 200 Hz Q=1.
|
||
Affects the live guitar only — backing track is untouched.
|
||
</p>
|
||
</div>
|
||
|
||
<!-- Signal Chain -->
|
||
<div class="mb-6">
|
||
<h3 class="text-lg font-semibold mb-3 text-slate-200">Signal Chain</h3>
|
||
<div id="ae-chain" class="space-y-2 mb-3">
|
||
<div class="text-sm text-slate-500 italic">No processors loaded</div>
|
||
</div>
|
||
<div class="flex gap-2 flex-wrap">
|
||
<button id="ae-add-vst" class="px-3 py-1.5 rounded bg-purple-600 hover:bg-purple-500 text-sm">
|
||
+ VST Plugin
|
||
</button>
|
||
<button id="ae-add-nam" class="px-3 py-1.5 rounded bg-orange-600 hover:bg-orange-500 text-sm">
|
||
+ NAM Model
|
||
</button>
|
||
<button id="ae-add-ir" class="px-3 py-1.5 rounded bg-cyan-600 hover:bg-cyan-500 text-sm">
|
||
+ Cabinet IR
|
||
</button>
|
||
<button id="ae-clear-chain" class="px-3 py-1.5 rounded bg-slate-600 hover:bg-slate-500 text-sm">
|
||
Clear All
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- VST Plugin Browser -->
|
||
<div id="ae-vst-browser" class="mb-6 hidden">
|
||
<h3 class="text-lg font-semibold mb-3 text-slate-200">VST Plugins</h3>
|
||
<div class="flex gap-2 mb-3">
|
||
<button id="ae-scan-vsts" class="px-3 py-1.5 rounded bg-slate-600 hover:bg-slate-500 text-sm">
|
||
Scan for Plugins
|
||
</button>
|
||
<input type="text" id="ae-vst-search" placeholder="Search plugins..."
|
||
class="flex-1 bg-slate-700 border border-slate-600 rounded px-3 py-1.5 text-sm">
|
||
</div>
|
||
<div id="ae-vst-list" class="max-h-64 overflow-y-auto space-y-1">
|
||
<div class="text-sm text-slate-500 italic">Click "Scan for Plugins" to find installed VST plugins</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Pitch Detection Display -->
|
||
<div class="mb-6">
|
||
<h3 class="text-lg font-semibold mb-3 text-slate-200">Pitch Detection</h3>
|
||
<div class="flex items-center gap-6">
|
||
<div class="text-center">
|
||
<div id="ae-pitch-note" class="text-4xl font-bold text-emerald-400">--</div>
|
||
<div id="ae-pitch-freq" class="text-xs text-slate-400">-- Hz</div>
|
||
</div>
|
||
<div class="flex-1">
|
||
<div class="relative h-8 bg-slate-800 rounded">
|
||
<div class="absolute left-1/2 top-0 bottom-0 w-0.5 bg-slate-500"></div>
|
||
<div id="ae-pitch-cents" class="absolute top-1 bottom-1 w-2 bg-emerald-400 rounded transition-all duration-75"
|
||
style="left:50%"></div>
|
||
</div>
|
||
<div class="flex justify-between text-xs text-slate-500 mt-1">
|
||
<span>-50c</span>
|
||
<span>0</span>
|
||
<span>+50c</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Preset Management -->
|
||
<div class="mb-6">
|
||
<h3 class="text-lg font-semibold mb-3 text-slate-200">Presets</h3>
|
||
<div class="flex gap-2">
|
||
<button id="ae-save-preset" class="px-3 py-1.5 rounded bg-slate-600 hover:bg-slate-500 text-sm">
|
||
Save Current Chain
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|