diff --git a/src/audio/AudioEngine.cpp b/src/audio/AudioEngine.cpp index 84949a0..4f258ea 100644 --- a/src/audio/AudioEngine.cpp +++ b/src/audio/AudioEngine.cpp @@ -228,6 +228,54 @@ double AudioEngine::getLatencyMs() const return (totalSamples / sr) * 1000.0; } +AudioEngine::LatencyBreakdown AudioEngine::getLatencyBreakdown() const +{ + LatencyBreakdown b; + const double sr = currentSampleRate.load(std::memory_order_relaxed); + b.sampleRate = sr; + b.duplex = duplexMode.load(std::memory_order_relaxed); + if (sr <= 0.0) return b; + const auto ms = [sr](double samples) { return (samples / sr) * 1000.0; }; + + if (b.duplex) + { + if (auto* device = inputDeviceManager.getCurrentAudioDevice()) + { + b.deviceBufferMs = ms(device->getCurrentBufferSizeSamples()); + b.inputLatencyMs = ms(device->getInputLatencyInSamples()); + b.outputLatencyMs = ms(device->getOutputLatencyInSamples()); + } + } + else + { + if (auto* in = inputDeviceManager.getCurrentAudioDevice()) + { + b.deviceBufferMs += ms(in->getCurrentBufferSizeSamples()); + b.inputLatencyMs = ms(in->getInputLatencyInSamples()); + } + if (auto* out = outputDeviceManager.getCurrentAudioDevice()) + { + b.deviceBufferMs += ms(out->getCurrentBufferSizeSamples()); + b.outputLatencyMs = ms(out->getOutputLatencyInSamples()); + } + // MEASURED ring residency — getLatencyMs() still reports the static + // half-capacity estimate for compatibility; this is the live figure + // getDeviceMetrics already exposes, converted to time. + const uint64_t w = outputRing.writeIndex.load(std::memory_order_acquire); + const uint64_t r = outputRing.readIndex.load(std::memory_order_acquire); + const uint64_t fill = (w >= r) ? (w - r) : 0; + b.splitRingMs = ms((double) std::min(fill, (uint64_t) kOutputRingFrames)); + } + b.monitorTotalMs = b.deviceBufferMs + b.inputLatencyMs + b.outputLatencyMs + b.splitRingMs; + + // Song audio over the renderer bus is delayed by the measured bus fill — + // a term no previous latency figure surfaced (deep-read 5). + const auto busMetrics = rendererBus.metrics(); + if (busMetrics.enabled) + b.rendererBusMs = ms((double) busMetrics.fillFrames); + return b; +} + // ── Device Selection ────────────────────────────────────────────────────────── bool AudioEngine::setDeviceType(const juce::String& typeName) diff --git a/src/audio/AudioEngine.h b/src/audio/AudioEngine.h index c0999bf..a2db77b 100644 --- a/src/audio/AudioEngine.h +++ b/src/audio/AudioEngine.h @@ -280,6 +280,25 @@ public: // Latency double getLatencyMs() const; + // One owner for every latency term (TLC deep-read §5) — the previous + // three unreconciled truths were getLatencyMs' static half-capacity ring + // guess, the verifier's input-latency-delta-only offset, and the renderer + // bus adding prime+fill+resample that no figure surfaced. All ms. + struct LatencyBreakdown + { + double sampleRate = 0.0; + bool duplex = true; + double deviceBufferMs = 0.0; // input buffer (+ output buffer when split) + double inputLatencyMs = 0.0; // driver-reported capture latency + double outputLatencyMs = 0.0; // driver-reported playback latency + double splitRingMs = 0.0; // MEASURED primary-ring residency (0 in duplex) + double monitorTotalMs = 0.0; // guitar in→out: buffers + in/out + splitRing + // Renderer-bus song-audio delay: measured bus fill (includes the + // ~10.7 ms prime cushion once flowing). 0 when the bus is off. + double rendererBusMs = 0.0; + }; + LatencyBreakdown getLatencyBreakdown() 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 diff --git a/src/audio/NodeAddon.cpp b/src/audio/NodeAddon.cpp index f05ce03..503b8e7 100644 --- a/src/audio/NodeAddon.cpp +++ b/src/audio/NodeAddon.cpp @@ -109,6 +109,7 @@ using slopsmith::addon::SetMonitorMuteSuppressed; using slopsmith::addon::AcquireMonitorMuteHold; using slopsmith::addon::ReleaseMonitorMuteHold; using slopsmith::addon::GetMonitorMuteState; +using slopsmith::addon::GetLatencyBreakdown; using slopsmith::addon::SetMultiBypass; using slopsmith::addon::SetNoiseGate; using slopsmith::addon::SetNoteDetectionEnabled; @@ -352,6 +353,7 @@ static Napi::Object InitModule(Napi::Env env, Napi::Object exports) exports.Set("acquireMonitorMuteHold", Napi::Function::New(env, AcquireMonitorMuteHold)); exports.Set("releaseMonitorMuteHold", Napi::Function::New(env, ReleaseMonitorMuteHold)); exports.Set("getMonitorMuteState", Napi::Function::New(env, GetMonitorMuteState)); + exports.Set("getLatencyBreakdown", Napi::Function::New(env, GetLatencyBreakdown)); exports.Set("isMonitorMuted", Napi::Function::New(env, IsMonitorMuted)); exports.Set("setMonitorKill", Napi::Function::New(env, SetMonitorKill)); exports.Set("setNoiseGate", Napi::Function::New(env, SetNoiseGate)); diff --git a/src/audio/addon/Bindings.h b/src/audio/addon/Bindings.h index 641ca4c..d53f754 100644 --- a/src/audio/addon/Bindings.h +++ b/src/audio/addon/Bindings.h @@ -11,6 +11,8 @@ class SourceChain; namespace slopsmith::addon { +Napi::Value GetLatencyBreakdown(const Napi::CallbackInfo& info); + // Validate a JS source-id argument and return the live source (nullptr for // missing / non-Number / non-finite / out-of-range). Shared by the // source-indexed bindings across the split files. diff --git a/src/audio/addon/DeviceBindings.cpp b/src/audio/addon/DeviceBindings.cpp index 0f2fb78..f68d1d2 100644 --- a/src/audio/addon/DeviceBindings.cpp +++ b/src/audio/addon/DeviceBindings.cpp @@ -831,4 +831,22 @@ Napi::Value GetSampleRate(const Napi::CallbackInfo& info) } +Napi::Value GetLatencyBreakdown(const Napi::CallbackInfo& info) +{ + auto env = info.Env(); + auto obj = Napi::Object::New(env); + auto liveEngine = snapshotEngine(); + if (!liveEngine) return obj; + const auto b = liveEngine->getLatencyBreakdown(); + obj.Set("sampleRate", b.sampleRate); + obj.Set("duplex", b.duplex); + obj.Set("deviceBufferMs", b.deviceBufferMs); + obj.Set("inputLatencyMs", b.inputLatencyMs); + obj.Set("outputLatencyMs", b.outputLatencyMs); + obj.Set("splitRingMs", b.splitRingMs); + obj.Set("monitorTotalMs", b.monitorTotalMs); + obj.Set("rendererBusMs", b.rendererBusMs); + return obj; +} + } // namespace slopsmith::addon diff --git a/tests/contracts/addon-exports.json b/tests/contracts/addon-exports.json index dd85205..a4b7e17 100644 --- a/tests/contracts/addon-exports.json +++ b/tests/contracts/addon-exports.json @@ -17,6 +17,7 @@ "getDeviceMetrics", "getDeviceTypes", "getKnownPlugins", + "getLatencyBreakdown", "getLevels", "getMonitorMuteState", "getNoteVerdicts",