feat(audio): getLatencyBreakdown — one owner for every latency term

Deep-read §5: latency had three unreconciled truths — getLatencyMs' static
half-capacity ring guess (42.7 ms), the verifier's input-delta-only offset,
and the renderer bus adding prime+fill+resample that no figure surfaced.

New engine API + export: per-term breakdown (deviceBufferMs, input/output
driver latency, MEASURED split-ring residency, monitor total) plus the
renderer-bus song-audio delay (measured bus fill) as its own term. On the
user's split exclusive setup the measured ring sits at ~10 ms — the legacy
figure overstated monitor latency by ~33 ms (102.7 reported vs ~70 real).

getLatencyMs is unchanged for compatibility; UI adoption of the breakdown
is renderer follow-up. Snapshots regenerated (104 exports).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
OmikronApex
2026-07-14 03:05:19 +02:00
co-authored by Claude Fable 5
parent f0340ed425
commit 9d0963d6d5
6 changed files with 90 additions and 0 deletions
+48
View File
@@ -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<uint64_t>(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)
+19
View File
@@ -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
+2
View File
@@ -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));
+2
View File
@@ -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.
+18
View File
@@ -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
+1
View File
@@ -17,6 +17,7 @@
"getDeviceMetrics",
"getDeviceTypes",
"getKnownPlugins",
"getLatencyBreakdown",
"getLevels",
"getMonitorMuteState",
"getNoteVerdicts",