mirror of
https://github.com/got-feedBack/feedBack-desktop.git
synced 2026-09-11 02:34:10 +00:00
The old single monitorMuted atomic had five writers fighting last-writer-wins: the settings checkbox, startup restore, the executor's preload read-force-restore, releaseRoute's unconditional setMonitorMute(true) (which clobbered the user's persisted preference), and the renderer's song-load suppression (un-refcounted — overlapping windows un-suppressed each other early). Native arbiter on SourceChain: userMonitorMute (the preference — checkbox + restore only), refcounted monitorMuteHolds (force-mute overrides), and refcounted suppressions (setMonitorMuteSuppressed keeps its bool surface; true=acquire, false=release, clamped at 0). Effective dry-mute = (holds || pref) && chain empty && no suppression — the suppressed-beats-muted precedence is unchanged. New exports: acquire/releaseMonitorMuteHold, getMonitorMuteState (diag); snapshots regenerated. Executor rewrite: acquires a suppression (dry-during-load, the default) or a hold, and releases exactly what it acquired via a single-fire closure that runs UNCONDITIONALLY (each load owns its acquisition — the stale-snapshot race against a mid-hold user toggle is structurally gone). releaseRoute no longer touches mute state at all. The ownership test now pins: preference API never called, acquire/release balanced. Renderer callers are unchanged: the checkbox writes the preference as before, and the song-load suppression sites now compose instead of racing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
515 lines
28 KiB
C++
515 lines
28 KiB
C++
#pragma once
|
|
#include "SourceChain.h"
|
|
#include "GainSanitize.h"
|
|
#include "engine/PackedStereoRing.h"
|
|
#include "engine/EngineState.h"
|
|
#include "engine/RendererBus.h"
|
|
#include "engine/StreamSink.h"
|
|
#include "engine/BackingPlayer.h"
|
|
#include "engine/DeviceSetup.h"
|
|
#include "engine/SourcePool.h"
|
|
#include "engine/ExtraInputs.h"
|
|
#include "BackingLeveler.h"
|
|
#include "signalsmith-stretch.h"
|
|
#include <juce_audio_devices/juce_audio_devices.h>
|
|
#include <juce_audio_formats/juce_audio_formats.h>
|
|
#include <array>
|
|
#include <atomic>
|
|
#include <bit>
|
|
#include <cmath>
|
|
#include <cstdint>
|
|
#include <memory>
|
|
#include <mutex>
|
|
#include <vector>
|
|
|
|
class AudioEngine : private juce::AudioIODeviceCallback
|
|
{
|
|
public:
|
|
AudioEngine();
|
|
~AudioEngine() override;
|
|
|
|
juce::AudioDeviceManager& getDeviceManager() { return inputDeviceManager; }
|
|
juce::AudioDeviceManager& getInputDeviceManager() { return inputDeviceManager; }
|
|
juce::AudioDeviceManager& getOutputDeviceManager() { return outputDeviceManager; }
|
|
// Per-input DSP now lives on a SourceChain; the engine owns sources[0] (the
|
|
// legacy default input) and forwards the single-source API to it. Multi-source
|
|
// fan-out (sources[1..N]) lands in a later phase; the public surface here is
|
|
// unchanged so NodeAddon and the renderer need no change.
|
|
SignalChain& getSignalChain() { return source0().getSignalChain(); }
|
|
PitchDetector& getPitchDetector() { return source0().getPitchDetector(); }
|
|
MlNoteDetector& getMlNoteDetector() { return source0().getMlNoteDetector(); }
|
|
|
|
// Arm/suspend the ML note-detection pipeline across every source's detector.
|
|
// Defaults off; the renderer (note_detect) calls this true only while a
|
|
// consumer actually reads ML notes (native-frame detection / non-verifier
|
|
// fallback) and false otherwise, so the default harmonic-comb verifier path
|
|
// — and the always-on home tuner — never pay for ONNX inference. Main thread.
|
|
void setMlNoteDetectionEnabled(bool e);
|
|
|
|
// Load the Basic Pitch ONNX model for the polyphonic ML detector. When a
|
|
// model is loaded, getActiveDetection() / scoreChord() route through it;
|
|
// otherwise they fall back to the YIN PitchDetector / ChordScorer.
|
|
bool loadNoteModel(const juce::File& modelFile) { return source0().loadNoteModel(modelFile); }
|
|
bool hasMlNoteDetector() const { return source0().hasMlNoteDetector(); }
|
|
|
|
// Best current single-note detection: the ML detector's dominant pitch
|
|
// when a model is loaded, else the YIN detector's latest result. Shape is
|
|
// identical either way so the getPitchDetection bridge is detector-agnostic.
|
|
PitchDetector::Detection getActiveDetection() const { return source0().getActiveDetection(); }
|
|
|
|
// Raw monophonic YIN detection, always — bypasses the ML preference so the
|
|
// continuous frequency (sub-Hz, parabolically interpolated) and real cents
|
|
// survive even when a Basic Pitch model is loaded. Backs the tuner's
|
|
// getRawPitch bridge endpoint; the YIN detector reads the post-noise-gate
|
|
// signal, so this is silent (frequency -1) when the gate is closed.
|
|
PitchDetector::Detection getRawPitchDetection() const { return source0().getRawPitchDetection(); }
|
|
|
|
// Device enumeration
|
|
struct DeviceTypeInfo
|
|
{
|
|
juce::String name;
|
|
juce::StringArray inputDevices;
|
|
juce::StringArray outputDevices;
|
|
};
|
|
// Device-config shapes moved to engine/DeviceSetup.h (TLC phase 4);
|
|
// aliased so the AudioEngine::DeviceOptions etc. spelling NodeAddon uses
|
|
// is unchanged.
|
|
using DeviceOptions = slopsmith::DeviceOptions;
|
|
using DeviceConfig = slopsmith::DeviceConfig;
|
|
using DeviceConfigResult = slopsmith::DeviceConfigResult;
|
|
|
|
struct DeviceMetrics
|
|
{
|
|
uint64_t inputOverflowCount = 0;
|
|
uint64_t outputUnderflowCount = 0;
|
|
// Counts are in audio frames (stereo pairs), not interleaved-float
|
|
// samples — the ring stores 2 floats per slot but the index math
|
|
// and consumer-facing health metric tick once per frame.
|
|
int outputRingFillFrames = 0;
|
|
int outputRingCapacityFrames = 0;
|
|
bool duplex = true;
|
|
};
|
|
|
|
juce::Array<DeviceTypeInfo> getDeviceTypes();
|
|
|
|
// Phase 2: input devices the user can bind as an ADDITIONAL engine input —
|
|
// restricted to the PRIMARY input's device type (so a JACK pick can't collide
|
|
// with an ALSA primary), minus the device already open as the primary (that's
|
|
// "Main") and minus monitor/loopback pseudo-inputs. Keeps the per-panel device
|
|
// picker to a compatible, sensible set instead of every capture node.
|
|
using BindableInput = slopsmith::ExtraInputs::Bindable;
|
|
std::vector<BindableInput> getBindableInputDevices();
|
|
|
|
juce::Array<double> getSampleRates();
|
|
juce::Array<int> getBufferSizes();
|
|
DeviceOptions probeDeviceOptions(const juce::String& typeName,
|
|
const juce::String& inputName,
|
|
const juce::String& outputName);
|
|
DeviceOptions probeDeviceOptionsDual(const juce::String& inputTypeName,
|
|
const juce::String& inputName,
|
|
const juce::String& outputTypeName,
|
|
const juce::String& outputName);
|
|
juce::String getCurrentDeviceType(); // = getCurrentInputDeviceType
|
|
juce::String getCurrentInputDeviceType();
|
|
juce::String getCurrentOutputDeviceType();
|
|
juce::String getCurrentInputDevice();
|
|
juce::String getCurrentOutputDevice();
|
|
bool isDuplex() const { return duplexMode.load(std::memory_order_relaxed); }
|
|
double getCurrentSampleRate() const { return currentSampleRate.load(std::memory_order_relaxed); }
|
|
int getCurrentBlockSize() const { return inputBlockSize.load(std::memory_order_relaxed); }
|
|
int getCurrentInputBlockSize() const { return inputBlockSize.load(std::memory_order_relaxed); }
|
|
int getCurrentOutputBlockSize() const { return outputBlockSize.load(std::memory_order_relaxed); }
|
|
DeviceMetrics getDeviceMetrics() const;
|
|
|
|
bool setDeviceType(const juce::String& typeName);
|
|
bool setInputDeviceType(const juce::String& typeName) { return setDeviceType(typeName); }
|
|
bool setOutputDeviceType(const juce::String& typeName);
|
|
bool setAudioDevice(const juce::String& inputName, const juce::String& outputName,
|
|
double sampleRate = 48000.0, int bufferSize = 256);
|
|
DeviceConfigResult setAudioDevices(const DeviceConfig& config);
|
|
|
|
// Audio start/stop
|
|
void startAudio();
|
|
void stopAudio();
|
|
bool isAudioRunning() const { return audioRunning.load(std::memory_order_relaxed); }
|
|
|
|
// Gain controls. Input + chain-output gain are per-source (sources[0]);
|
|
// output gain is the post-mix master and stays engine-global.
|
|
void setInputGain(float gain) { source0().setInputGain(gain); }
|
|
// Sanitized (see GainSanitize.h): a NaN/Inf master gain from JS would
|
|
// multiply the whole device output to NaN downstream of the per-source
|
|
// scrub — clamp at the store so every caller is covered.
|
|
void setOutputGain(float gain) { outputGain.store(slopsmith::sanitizeMasterGain(gain)); }
|
|
float getInputGain() const { return source0().getInputGain(); }
|
|
float getOutputGain() const { return outputGain.load(); }
|
|
|
|
// Chain output gain — the amp/tone's output level, applied to the guitar
|
|
// signal before the backing track is mixed. Distinct from outputGain (the
|
|
// post-mix master) so a tone-preset switch doesn't move the song volume.
|
|
void setChainOutputGain(float gain) { source0().setChainOutputGain(gain); }
|
|
float getChainOutputGain() const { return source0().getChainOutputGain(); }
|
|
|
|
// Input channel selection (for multi-channel interfaces like Valeton GP-5)
|
|
// 0=left (dry), 1=right (wet), -1=both (mono mix)
|
|
void setInputChannel(int channel) { source0().setInputChannel(channel); }
|
|
int getInputChannel() const { return source0().getInputChannel(); }
|
|
|
|
// Monitor mute — when true, input is still processed (pitch detection, metering)
|
|
// but output is silenced unless there are processors in the signal chain
|
|
void setMonitorMute(bool mute) { source0().setMonitorMute(mute); }
|
|
bool isMonitorMuted() const { return source0().isMonitorMuted(); }
|
|
|
|
// Monitor-mute suppression — when true, the monitor mute is temporarily
|
|
// overridden so the dry guitar stays audible even with an empty chain.
|
|
// The renderer sets this around a song-load chain rebuild (clear + reload),
|
|
// so the brief empty-chain window doesn't silence the player's guitar.
|
|
void setMonitorMuteSuppressed(bool suppressed) { source0().setMonitorMuteSuppressed(suppressed); }
|
|
bool isMonitorMuteSuppressed() const { return source0().isMonitorMuteSuppressed(); }
|
|
// Refcounted force-mute overrides (see SourceChain's arbiter comment).
|
|
void acquireMonitorMuteHold() { source0().acquireMonitorMuteHold(); }
|
|
void releaseMonitorMuteHold() { source0().releaseMonitorMuteHold(); }
|
|
int getMonitorMuteHoldCount() const { return source0().getMonitorMuteHoldCount(); }
|
|
int getMonitorMuteSuppressCount() const { return source0().getMonitorMuteSuppressCount(); }
|
|
|
|
// Full monitor kill — silences the guitar bus entirely (dry + processed),
|
|
// for monitoring through an external rig. Unlike the per-source mute/gain
|
|
// controls (which delegate to source0()), this is a GLOBAL "play through my
|
|
// own rig" preference, so it's applied to EVERY pooled source — active or
|
|
// not — so additional inputs are silenced too and a later addSource()
|
|
// inherits it (addSource never resets the flag). The fixed pool's pointers
|
|
// are never reassigned, and these are plain atomic stores, so iterating it
|
|
// off the control thread is race-free. Default off; see SourceChain.
|
|
void setMonitorKill(bool kill)
|
|
{
|
|
pool.forEach([kill](SourceChain& s) { s.setMonitorKill(kill); });
|
|
}
|
|
bool isMonitorKilled() const { return source0().isMonitorKilled(); }
|
|
|
|
// Number of audio blocks whose signal-chain output had to be scrubbed for
|
|
// non-finite/runaway samples (issue #403). A nonzero value means the chain
|
|
// (NAM/IR/VST) emitted garbage that was contained before it reached the
|
|
// output. Exposed for diagnostics.
|
|
uint32_t getNonFiniteChainBlocks() const { return source0().getNonFiniteChainBlocks(); }
|
|
|
|
// Noise gate (post-input-gain, pre FX chain; pitch detector sees ungated signal)
|
|
void setNoiseGate(bool enabled, float thresholdDb, float releaseMs, float depthDb)
|
|
{
|
|
source0().setNoiseGate(enabled, thresholdDb, releaseMs, depthDb);
|
|
}
|
|
|
|
// Tone Polish — fixed 3-band mastering EQ (HPF 80 Hz, low shelf -3 dB
|
|
// @ 180 Hz, peak -0.5 dB @ 200 Hz Q=1). Applied on the guitar bus only,
|
|
// between chainOutputGain and the backing-track mix, so the backing
|
|
// track and master output gain stay bit-untouched. Defaults on;
|
|
// renderer exposes a per-preset toggle.
|
|
void setTonePolishEnabled(bool enabled) { source0().setTonePolishEnabled(enabled); }
|
|
|
|
// Backing track — transport moved to engine/BackingPlayer (TLC phase 3);
|
|
// the volume fader + level meter stay engine-side (mix policy).
|
|
void setBackingVolume(float vol) { backingVolume.store(slopsmith::sanitizeMasterGain(vol)); }
|
|
bool loadBackingTrack(const juce::File& file)
|
|
{
|
|
currentBackingLevel.store(0.0f);
|
|
return backing.load(file);
|
|
}
|
|
void setBackingPosition(double seconds) { backing.setPosition(seconds); }
|
|
void startBacking() { backing.start(); }
|
|
void stopBacking()
|
|
{
|
|
backing.stop();
|
|
currentBackingLevel.store(0.0f);
|
|
}
|
|
void setBackingSpeed(double speed) { backing.setSpeed(speed); }
|
|
// Non-blocking reads — never acquire the backing lock / block the audio callback
|
|
bool isBackingPlaying() const { return backing.isPlaying(); }
|
|
double getBackingPosition() const { return backing.getPosition(); }
|
|
double getBackingDuration() const { return backing.getDuration(); }
|
|
|
|
// Metering (read from any thread — atomic). Input level/peak are per-source
|
|
// (sources[0]); output level/peak are the post-mix master, engine-global.
|
|
float getInputLevel() const { return source0().getInputLevel(); }
|
|
float getOutputLevel() const { return currentOutputLevel.load(); }
|
|
float getInputPeak() const { return source0().getInputPeak(); }
|
|
float getOutputPeak() const { return outputPeak.load(); }
|
|
// Running RMS of the backing-track mix bus after the volume fader, updated
|
|
// each audio block by the audio thread. Safe to call from any thread.
|
|
float getBackingLevel() const { return currentBackingLevel.load(); }
|
|
void resetPeaks();
|
|
|
|
// ── Streamer mix output (PR1: one stream bus → one extra output device) ───
|
|
// An ADDITIONAL output device carrying an independent submix (game/backing +
|
|
// the guitar monitor mix) for OBS/Discord capture, separate from the local
|
|
// monitor output. Default off → zero behaviour change. Control-thread only.
|
|
// setStreamOutputDevice returns "" on success or an error string.
|
|
juce::String setStreamOutputDevice(const juce::String& typeName, const juce::String& deviceName);
|
|
void clearStreamOutput();
|
|
bool isStreamOutputActive() const { return streamSink.isActive(); }
|
|
juce::String getStreamOutputDeviceName() const { return streamSink.getDesiredDeviceName(); }
|
|
// Bus content: include the backing/game, include the guitar monitor mix, and a
|
|
// linear output gain. All atomic — safe to set live. Gain is sanitised
|
|
// (finite, clamped 0..8) so a NaN/Inf from JS can never reach the stream ring.
|
|
void setStreamBus(bool includeBacking, bool includeGuitar, float gain) { streamSink.setBus(includeBacking, includeGuitar, gain); }
|
|
void setStreamBusGain(float gain) { streamSink.setBusGain(gain); }
|
|
|
|
// ── Renderer-audio bus (Phase 2: WebAudio master → engine output) ─────────
|
|
// The renderer pushes its WebAudio master mix here (via IPC) so song/stem
|
|
// audio stays audible when the output device is exclusive-style and the OS
|
|
// mixer path is silenced. SPSC: producer is the main-process IPC thread,
|
|
// consumer is whichever output callback is live (duplex or split). Default
|
|
// off → zero behaviour change.
|
|
void setRendererBus(bool enabled, float gain) { rendererBus.setEnabled(enabled, gain); }
|
|
// Interleaved stereo frames at `sourceRate`; linear-resampled to the device
|
|
// rate on the producer thread (fractional position + previous frame carried
|
|
// across calls). Returns false when the bus is disabled or the engine is
|
|
// not running. Drop-oldest on overflow, counted.
|
|
bool pushRendererAudio(const float* interleavedLR, int frames, double sourceRate);
|
|
struct RendererBusMetrics
|
|
{
|
|
uint64_t pushedFrames = 0, consumedFrames = 0, underflowCount = 0, overflowCount = 0;
|
|
int fillFrames = 0, capacityFrames = 0;
|
|
bool enabled = false;
|
|
};
|
|
RendererBusMetrics getRendererBusMetrics() const;
|
|
|
|
float getStreamSinkLevel() const { return streamSink.getLevel(); }
|
|
uint64_t getStreamUnderflowCount() const { return streamSink.getUnderflowCount(); }
|
|
// Producer overflow (drop-oldest): the consumer fell a full ring behind and
|
|
// frames were skipped. Exposed alongside underflow for stream drift diagnosis.
|
|
uint64_t getStreamOverflowCount() const { return streamSink.getOverflowCount(); }
|
|
|
|
// Latency
|
|
double getLatencyMs() const;
|
|
|
|
// Raw input frame snapshot for renderer-side polyphonic chord scoring in
|
|
// notedetect. Backed by sources[0]'s pre-gate input ring; the rings (and the
|
|
// power-of-two capacity constants) now live on SourceChain. Default snapshot
|
|
// size matches notedetect's _ND_MIN_YIN_SAMPLES (4096 samples).
|
|
std::vector<float> getInputFrame(int numSamples = 4096) const { return source0().getInputFrame(numSamples); }
|
|
|
|
// Gapless input-ring consumption for the onset detector — consecutive calls
|
|
// consume each sample exactly once. See SourceChain::getInputSince for the
|
|
// full gap/shortfall contract.
|
|
uint64_t getInputSince(uint64_t fromIndex, std::vector<float>& out) const { return source0().getInputSince(fromIndex, out); }
|
|
|
|
// Post-noise-gate raw mono audio snapshot for the external tuner plugin
|
|
// (distinct from getInputFrame's pre-gate ring). Backed by sources[0].
|
|
std::vector<float> getRawAudioFrame(int numSamples = 4096) const { return source0().getRawAudioFrame(numSamples); }
|
|
|
|
// Score a chord against the latest input-ring samples. The chord context
|
|
// (notes, arrangement, thresholds) comes from the renderer over IPC; audio
|
|
// data stays inside the engine. Same `{score, hitStrings, totalStrings,
|
|
// isHit, results[]}` shape as the JS implementation.
|
|
ChordScorer::Result scoreChord(const ChordScorer::Request& req) { return source0().scoreChord(req); }
|
|
|
|
// Continuous engine-side chart verification (notedetect). The renderer
|
|
// pushes the song's note chart once via setChart(); a background
|
|
// NoteVerifier thread scores each note's timing window against the live
|
|
// playhead and input ring, and the renderer drains finalized verdicts
|
|
// via getNoteVerdicts(). This replaces the renderer's per-tick
|
|
// scoreChord IPC loop, which starved during dense passages.
|
|
void setChart(const NoteVerifier::ChartUpdate& chart) { source0().setChart(chart); }
|
|
void clearChart() { source0().clearChart(); }
|
|
std::vector<NoteVerifier::Verdict> getNoteVerdicts() { return source0().getNoteVerdicts(); }
|
|
|
|
// Renderer's unified, already-corrected playhead — the verifier scores
|
|
// against this rather than getBackingPosition(), which is frozen for
|
|
// HTML5-routed (sloppak) songs. Pushed each detect tick via getNoteVerdicts.
|
|
void setPlayhead(double songTime, bool playing) { source0().setPlayhead(songTime, playing); }
|
|
|
|
// ── Multi-input source management ─────────────────────────────────────────
|
|
// A "source" is one independent input chain (its own arrangement chart, note
|
|
// detection, scoring, tone, and monitor). sources[0] always exists. Adding a
|
|
// source binds it to an input channel of the current device (multi-channel
|
|
// interface); separate-device binding lands in a later phase.
|
|
struct SourceInfo
|
|
{
|
|
int id = -1;
|
|
int inputChannel = -1; // -1 = mono mix of first pair
|
|
int deviceKey = 0; // 0 = primary input device
|
|
bool active = false;
|
|
};
|
|
|
|
// Activate a pooled chain bound to `inputChannel` of input device `deviceKey`
|
|
// (0 = primary device) and return its id, or -1 if the pool is full. Prepares
|
|
// the chain immediately when audio is running so it starts scoring without a
|
|
// device restart. Control-thread only.
|
|
int addSource(int inputChannel, int deviceKey = 0);
|
|
// Deactivate + release a source (id != 0; sources[0] is permanent). Stops its
|
|
// verifier/ML threads; the pooled object is reused by a later addSource.
|
|
bool removeSource(int id);
|
|
// Snapshot of every active source. Control-thread only.
|
|
std::vector<SourceInfo> listSources() const;
|
|
|
|
// Phase 2 (multi-device): open `deviceName` as an ADDITIONAL physical input
|
|
// device bound to `deviceKey` (1..kMaxExtraInputDevices) so sources created
|
|
// with addSource(channel, deviceKey) capture from it at its OWN clock. Forces
|
|
// split mode. Returns "" on success or an error string. unbind stops+releases
|
|
// it. activeExtraInputCount = # bound+running extras. Control-thread only.
|
|
juce::String bindInputDevice(int deviceKey, const juce::String& deviceName);
|
|
bool unbindInputDevice(int deviceKey);
|
|
int activeExtraInputCount() const;
|
|
|
|
// Per-source accessors for the NodeAddon source-indexed API. Return nullptr
|
|
// for an out-of-range or inactive id (sources[0] always valid).
|
|
SourceChain* getSource(int id);
|
|
|
|
private:
|
|
// sources[0] is the legacy default input chain; always present + active.
|
|
SourceChain& source0() { return pool.chain0(); }
|
|
const SourceChain& source0() const { return pool.chain0(); }
|
|
// Input-device callback. In duplex it writes outputData directly; in split
|
|
// it pushes processed stereo into outputRing for OutputCallback.
|
|
void audioDeviceIOCallbackWithContext(const float* const* inputData,
|
|
int numInputChannels,
|
|
float* const* outputData,
|
|
int numOutputChannels,
|
|
int numSamples,
|
|
const juce::AudioIODeviceCallbackContext& context) override;
|
|
void audioDeviceAboutToStart(juce::AudioIODevice* device) override;
|
|
void audioDeviceStopped() override;
|
|
|
|
// Split-mode only: drains outputRing, mixes backing, writes to device.
|
|
void audioOutputCallback(const float* const* inputData,
|
|
int numInputChannels,
|
|
float* const* outputData,
|
|
int numOutputChannels,
|
|
int numSamples);
|
|
void audioOutputAboutToStart(juce::AudioIODevice* device);
|
|
void audioOutputStopped();
|
|
|
|
class OutputCallback : public juce::AudioIODeviceCallback
|
|
{
|
|
public:
|
|
explicit OutputCallback(AudioEngine& e) : engine(e) {}
|
|
void audioDeviceIOCallbackWithContext(const float* const* inputData,
|
|
int numInputChannels,
|
|
float* const* outputData,
|
|
int numOutputChannels,
|
|
int numSamples,
|
|
const juce::AudioIODeviceCallbackContext&) override
|
|
{
|
|
engine.audioOutputCallback(inputData, numInputChannels, outputData, numOutputChannels, numSamples);
|
|
}
|
|
void audioDeviceAboutToStart(juce::AudioIODevice* device) override { engine.audioOutputAboutToStart(device); }
|
|
void audioDeviceStopped() override { engine.audioOutputStopped(); }
|
|
private:
|
|
AudioEngine& engine;
|
|
};
|
|
OutputCallback outputCallback{ *this };
|
|
|
|
// Probe/apply/teardown moved to engine/DeviceSetup (TLC phase 4);
|
|
// setAudioDevices stays here as the orchestrator.
|
|
void teardownSplitMode();
|
|
|
|
// Duplex mode: inputDeviceManager owns both directions, outputDeviceManager idle.
|
|
// Split mode: input-only on inputDeviceManager, output-only on outputDeviceManager
|
|
// with an SPSC ring between them.
|
|
juce::AudioDeviceManager inputDeviceManager;
|
|
juce::AudioDeviceManager outputDeviceManager;
|
|
|
|
// Shared run-state atomics (TLC phase 1) — the members below are
|
|
// reference aliases under their historical names so call sites are
|
|
// untouched; extracted units take `state` (EngineState&) directly.
|
|
slopsmith::EngineState state;
|
|
std::atomic<bool>& duplexMode = state.duplexMode;
|
|
// Probe/apply/teardown component (TLC phase 4). Holds references only.
|
|
slopsmith::DeviceSetup deviceSetup{ inputDeviceManager, outputDeviceManager, state };
|
|
|
|
// Per-input capture+detect+monitor chains + the add/remove/reclaim
|
|
// lifecycle + per-deviceKey quiescence handshake — moved to
|
|
// engine/SourcePool (TLC phase 5). Constants mirrored for the members
|
|
// that size arrays by them (extraInputs, and NodeAddon range checks).
|
|
static constexpr int kMaxSources = slopsmith::SourcePool::kMaxSources;
|
|
static constexpr int kMaxExtraInputDevices = slopsmith::SourcePool::kMaxExtraInputDevices;
|
|
slopsmith::SourcePool pool{ state };
|
|
// Audio-thread scratch for the multi-source mix on the PRIMARY callback:
|
|
// each active source renders its 2-channel monitor here in turn, then it
|
|
// is summed into the output. Pre-sized in audioDeviceAboutToStart so the
|
|
// hot loop never allocates. (Extra devices carry their own scratch.)
|
|
juce::AudioBuffer<float> sourceMonitorScratch;
|
|
|
|
// Master output (post-mix) — engine-global, not per-source.
|
|
std::atomic<float> outputGain{1.0f};
|
|
std::atomic<float> backingVolume{0.8f};
|
|
std::atomic<float> currentOutputLevel{0.0f};
|
|
// Per-block RMS of the backing-track mix bus, written by the audio thread
|
|
// and read on the main/JS thread via getBackingLevel(). Computed after the
|
|
// backing volume fader but before the output-gain master so VU meters reflect
|
|
// the track level independently of the post-mix master volume.
|
|
std::atomic<float> currentBackingLevel{0.0f};
|
|
std::atomic<float> outputPeak{0.0f};
|
|
|
|
// Backing track — transport/stretch/leveler moved to engine/BackingPlayer
|
|
// (TLC phase 3). Declared after `state` (bound by reference).
|
|
slopsmith::BackingPlayer backing{state};
|
|
|
|
// audioRunning keeps its historical DEVICE-STATE semantics (isAudioRunning
|
|
// compat pin); the intent half is state.userWantsAudio — see EngineState.h.
|
|
std::atomic<bool>& audioRunning = state.deviceRunning;
|
|
std::atomic<double>& currentSampleRate = state.currentSampleRate;
|
|
std::atomic<int>& inputBlockSize = state.inputBlockSize;
|
|
std::atomic<int>& outputBlockSize = state.outputBlockSize;
|
|
|
|
// The per-input lock-free SPSC rings (pre-gate getInputFrame ring + post-gate
|
|
// getRawAudioFrame ring), the YIN/ML detectors, and the zero-output capture
|
|
// scratch now live on SourceChain — one set per input source. See
|
|
// SourceChain.h for the full lock-free / power-of-two / cold-start rationale.
|
|
|
|
// Split-mode SPSC ring (unused in duplex). Packed-LR single-atomic frames
|
|
// — see engine/PackedStereoRing.h for the tear/lock-free rationale (moved
|
|
// there in TLC phase 1). ~85 ms @ 48 kHz — absorbs clock drift over
|
|
// typical sessions.
|
|
static constexpr int kOutputRingFrames = 4096;
|
|
slopsmith::PackedStereoRing<kOutputRingFrames> outputRing;
|
|
|
|
// ── Renderer-audio bus (see engine/RendererBus.h — moved in TLC phase 2)
|
|
slopsmith::RendererBus rendererBus;
|
|
// Shared consumer step for the duplex and split output paths: drain one
|
|
// block from the renderer-bus ring into `dest` (stereo, bus gain applied,
|
|
// dest cleared first). Returns numSamples on success, 0 when gated
|
|
// (disabled, priming, underflow, scratch undersized). Single consumer —
|
|
// call exactly once per output block; the caller mixes the pulled block
|
|
// into the device output AND hands it to composeAndPushStreamMix so the
|
|
// streamer submix carries renderer-fed song audio too.
|
|
int pullRendererBus(juce::AudioBuffer<float>& dest, int numSamples);
|
|
// Scratch for the per-block renderer-bus pull. Fixed capacity, sized once
|
|
// in about-to-start next to the stream scratches (same no-realloc rule).
|
|
juce::AudioBuffer<float> rendererBusPullScratch;
|
|
|
|
std::atomic<uint64_t> outputUnderflowCount{0};
|
|
std::atomic<uint64_t> inputOverflowCount{0};
|
|
|
|
// Pre-sized to outputBlockSize so the pull loop never allocates.
|
|
std::vector<float> outputPullScratchL;
|
|
std::vector<float> outputPullScratchR;
|
|
juce::AudioBuffer<float> outputBackingBuffer;
|
|
bool outputCallbackRegistered = false;
|
|
// Same guard for the primary INPUT callback (`this`): audioRunning can be
|
|
// cleared by a transient audioDeviceStopped() while the callback stays
|
|
// attached, and an unguarded startAudio() re-add would dispatch it twice
|
|
// per block (double DSP + double ring push → half-speed garbled audio) and
|
|
// leave a live registration behind after stopAudio()'s single remove.
|
|
bool inputCallbackRegistered = false;
|
|
|
|
// ── Additional input devices — moved to engine/ExtraInputs.{h,cpp}
|
|
// (TLC phase 5). The split output callback drains extraInputs.slots
|
|
// directly; declared after pool/state (bound by reference).
|
|
slopsmith::ExtraInputs extraInputs{ pool, state, inputDeviceManager };
|
|
using InputDeviceSlot = slopsmith::ExtraInputs::InputDeviceSlot;
|
|
|
|
// (mixSourcesForDevice moved to SourcePool::mixForDevice — TLC phase 5.)
|
|
|
|
// ── Streamer mix output sink — moved to engine/StreamSink.{h,cpp} (TLC
|
|
// phase 2). Declared after `state` (bound by reference).
|
|
slopsmith::StreamSink streamSink{state};
|
|
// Producer-side guitar monitor-mix snapshot (pre-backing), written by the
|
|
// primary/output callback and handed to streamSink.publish(). Sized in
|
|
// audioDeviceAboutToStart / audioOutputAboutToStart alongside the other scratch.
|
|
juce::AudioBuffer<float> streamGuitarScratch;
|
|
|
|
// Clamp a requested stream gain to a finite, sane range so a NaN/Inf (or a
|
|
// wild value) from the JS bridge can never be packed into the stream ring.
|
|
static float sanitizeStreamGain(float g) { return slopsmith::sanitizeStreamGain(g); }
|
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(AudioEngine)
|
|
};
|