mirror of
https://github.com/got-feedBack/feedBack-desktop.git
synced 2026-09-12 18:38:12 +00:00
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:
co-authored by
Claude Fable 5
parent
f0340ed425
commit
9d0963d6d5
@@ -228,6 +228,54 @@ double AudioEngine::getLatencyMs() const
|
|||||||
return (totalSamples / sr) * 1000.0;
|
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 ──────────────────────────────────────────────────────────
|
// ── Device Selection ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
bool AudioEngine::setDeviceType(const juce::String& typeName)
|
bool AudioEngine::setDeviceType(const juce::String& typeName)
|
||||||
|
|||||||
@@ -280,6 +280,25 @@ public:
|
|||||||
// Latency
|
// Latency
|
||||||
double getLatencyMs() const;
|
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
|
// 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
|
// 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
|
// power-of-two capacity constants) now live on SourceChain. Default snapshot
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ using slopsmith::addon::SetMonitorMuteSuppressed;
|
|||||||
using slopsmith::addon::AcquireMonitorMuteHold;
|
using slopsmith::addon::AcquireMonitorMuteHold;
|
||||||
using slopsmith::addon::ReleaseMonitorMuteHold;
|
using slopsmith::addon::ReleaseMonitorMuteHold;
|
||||||
using slopsmith::addon::GetMonitorMuteState;
|
using slopsmith::addon::GetMonitorMuteState;
|
||||||
|
using slopsmith::addon::GetLatencyBreakdown;
|
||||||
using slopsmith::addon::SetMultiBypass;
|
using slopsmith::addon::SetMultiBypass;
|
||||||
using slopsmith::addon::SetNoiseGate;
|
using slopsmith::addon::SetNoiseGate;
|
||||||
using slopsmith::addon::SetNoteDetectionEnabled;
|
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("acquireMonitorMuteHold", Napi::Function::New(env, AcquireMonitorMuteHold));
|
||||||
exports.Set("releaseMonitorMuteHold", Napi::Function::New(env, ReleaseMonitorMuteHold));
|
exports.Set("releaseMonitorMuteHold", Napi::Function::New(env, ReleaseMonitorMuteHold));
|
||||||
exports.Set("getMonitorMuteState", Napi::Function::New(env, GetMonitorMuteState));
|
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("isMonitorMuted", Napi::Function::New(env, IsMonitorMuted));
|
||||||
exports.Set("setMonitorKill", Napi::Function::New(env, SetMonitorKill));
|
exports.Set("setMonitorKill", Napi::Function::New(env, SetMonitorKill));
|
||||||
exports.Set("setNoiseGate", Napi::Function::New(env, SetNoiseGate));
|
exports.Set("setNoiseGate", Napi::Function::New(env, SetNoiseGate));
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ class SourceChain;
|
|||||||
|
|
||||||
namespace slopsmith::addon {
|
namespace slopsmith::addon {
|
||||||
|
|
||||||
|
Napi::Value GetLatencyBreakdown(const Napi::CallbackInfo& info);
|
||||||
|
|
||||||
// Validate a JS source-id argument and return the live source (nullptr for
|
// Validate a JS source-id argument and return the live source (nullptr for
|
||||||
// missing / non-Number / non-finite / out-of-range). Shared by the
|
// missing / non-Number / non-finite / out-of-range). Shared by the
|
||||||
// source-indexed bindings across the split files.
|
// source-indexed bindings across the split files.
|
||||||
|
|||||||
@@ -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
|
} // namespace slopsmith::addon
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
"getDeviceMetrics",
|
"getDeviceMetrics",
|
||||||
"getDeviceTypes",
|
"getDeviceTypes",
|
||||||
"getKnownPlugins",
|
"getKnownPlugins",
|
||||||
|
"getLatencyBreakdown",
|
||||||
"getLevels",
|
"getLevels",
|
||||||
"getMonitorMuteState",
|
"getMonitorMuteState",
|
||||||
"getNoteVerdicts",
|
"getNoteVerdicts",
|
||||||
|
|||||||
Reference in New Issue
Block a user