mirror of
https://github.com/got-feedBack/feedBack-desktop.git
synced 2026-09-11 01:44:11 +00:00
* fix(audio-input): stable name-based input identity + fail-loud open + bound read-back Replace the positional-index logicalSourceKey with a name-encoded one so a named device survives reorder/hotplug; resolve by name and fail loud instead of silently opening the default mic; read back and return the actually-bound device. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * perf(audio): gate the ML note-detection pipeline behind a master enable The Basic-Pitch ONNX detector is the most expensive thing in the engine (~30 ms inference every hop) and on the default desktop path nothing reads it: note detection is scored by the harmonic-comb NoteVerifier, and the always-on home tuner runs its own YIN over raw frames. Yet the pipeline ran unconditionally from construction, pinning a core on an idle home screen. Add a master gate so ML only runs when a consumer actually needs it: - MlNoteDetector: std::atomic<bool> enabled{false}. pushSamples() early- returns on the audio thread (lock-free relaxed load, no feed) and runInferenceIfDue() early-returns on the inference thread (no Run()), so the whole pipeline is dormant until armed. setEnabled(false) clears the rolling window + published snapshot (clearAudioState resets hasPublished), so a re-arm starts cold and serves the YIN fallback until the first fresh inference. The inference thread stays alive but idle — toggling needs no thread restart. isEnabled() for symmetry; no-op stubs in the ONNX-off build. - AudioEngine::setMlNoteDetectionEnabled(bool) fans to every source's detector (whole pool, so a later-activated source inherits the arm state). - NodeAddon setNoteDetectionEnabled + audio-bridge ipc + preload, all typeof/ try-guarded so a downlevel addon ignores it (fail-safe to current behaviour). The renderer (note_detect) arms this true only while it will read ML notes (native-frame detection / non-verifier fallback) and false otherwise — a follow-up renderer change. Default OFF means the shipped verifier path and the home tuner pay nothing for ML. Verified: native addon builds clean (ONNX path); the standalone mlnd_test detects the full C-major triad when armed (3/3); ml-note-detection + multi-source JS suites pass (16/16). mlnotedetector/test.cpp arms the detector after prepare() to match the new default. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(audio): make the ML gate reset race-free (thread-owned cold start) The first cut cleared the rolling window/FIFO from setEnabled() on the N-API thread while the inference thread was still alive — a data race on the buffers. Move the reset onto the thread that owns them, and fix two follow-on issues Codex flagged: - fifo.reset() TOCTOU: resetting the FIFO on the inference thread can still race an in-flight pushSamples() that passed the resetPending gate just before it was set (the >=8 ms callback gap is not a guarantee). Fix: the thread-side cold start DRAINS the FIFO (fifo.finishedRead(getNumReady()) — advances only the consumer's read index, safe SPSC) instead of fifo.reset(). clearAudioState() (with the real reset) is kept for the prepare()/stop() paths where the thread is already joined. resetPending stays set through the drain so pushSamples() is gated off the FIFO the whole time, then is released. - stale readiness on re-arm: setEnabled(true) exposed enabled=true immediately while hasPublished stayed true from the previous arm, so isReady() briefly served the old snapshot. Fix: drop hasPublished synchronously BEFORE storing enabled=true (release/acquire ordering: isReady() loads enabled before hasPublished, so seeing enabled=true guarantees seeing hasPublished=false). Other gate mechanics: the enabled-gate is at the top of the inference callback (disabled ⇒ no ingest, no inference), pushSamples() no-ops when !enabled or resetPending, and isReady() gates on enabled so a suspended detector serves the YIN fallback rather than a stale snapshot. mlnotedetector/test.cpp asserts both directions: fed the chord region while DISABLED, the detector publishes nothing and never becomes ready; armed, it still detects the full C-major triad (3/3). Addon rebuilds clean; tsc clean; ml-note-detection + multi-source JS suites pass (16/16). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
101 lines
4.5 KiB
C++
101 lines
4.5 KiB
C++
#pragma once
|
|
#include <juce_core/juce_core.h>
|
|
#include <atomic>
|
|
#include <memory>
|
|
#include <vector>
|
|
|
|
// Polyphonic ML note detector — Spotify Basic Pitch run via ONNX Runtime.
|
|
//
|
|
// Mirrors PitchDetector's threading contract: the audio thread pushes samples
|
|
// through a lock-free FIFO; a background thread resamples to 22050 Hz, runs
|
|
// inference on a rolling ~2 s window, and publishes an active-pitch snapshot
|
|
// that any non-audio thread can poll.
|
|
//
|
|
// When ONNX Runtime is not available at build time (SLOPSMITH_ONNX_SUPPORT=0)
|
|
// every method is an inert no-op and isAvailable() returns false — callers
|
|
// fall back to the YIN PitchDetector / ChordScorer (Constitution VII).
|
|
//
|
|
// The ONNX Runtime headers are confined to MlNoteDetector.cpp via a PIMPL, so
|
|
// including this header does not pull <onnxruntime_cxx_api.h> into the engine.
|
|
class MlNoteDetector
|
|
{
|
|
public:
|
|
MlNoteDetector();
|
|
~MlNoteDetector();
|
|
|
|
static constexpr int kNumPitches = 88; // Basic Pitch: MIDI 21..108
|
|
static constexpr int kLowestMidi = 21; // A0
|
|
|
|
// Load the Basic Pitch ONNX model. Thread-safe; the live model pointer is
|
|
// swapped under a lock once the new session is built (NAMProcessor pattern).
|
|
// Returns "is the ML detector available after this call" — NOT whether
|
|
// this particular load succeeded. A missing/invalid file never tears down
|
|
// an already-loaded model, so it can still return true; returns false when
|
|
// no model is loaded or ONNX support is compiled out.
|
|
bool loadModel(const juce::File& modelFile);
|
|
bool hasModel() const { return modelLoaded.load(std::memory_order_relaxed); }
|
|
|
|
// True only when ONNX support is compiled in AND a model is loaded
|
|
// (and its output contract has not been demoted).
|
|
bool isAvailable() const;
|
|
|
|
// isAvailable() AND the detector has published at least one inference
|
|
// snapshot — gate engine-side ML routing on this so the cold-start
|
|
// window after an audio start/restart uses the YIN fallback.
|
|
bool isReady() const;
|
|
|
|
void prepare(double sampleRate, int blockSize);
|
|
void stop();
|
|
|
|
// Master gate. Defaults OFF: the ML pipeline only runs inference (and only
|
|
// accepts pushSamples) while enabled. The renderer arms it via the
|
|
// setNoteDetectionEnabled bridge when a consumer actually needs ML notes
|
|
// (native-frame detection / non-verifier fallback); the default desktop path
|
|
// scores with the harmonic-comb NoteVerifier and leaves this off, so a home
|
|
// tuner — or an in-song verifier session — pays nothing for ML. Disabling
|
|
// clears the rolling window + published snapshot (cold re-arm); the inference
|
|
// thread stays alive but idle, so toggling needs no thread restart. Safe to
|
|
// call from the N-API/main thread; the audio thread reads the flag lock-free.
|
|
void setEnabled(bool e);
|
|
bool isEnabled() const;
|
|
|
|
// Audio thread — lock-free, no allocation.
|
|
void pushSamples(const float* data, int numSamples);
|
|
|
|
// A currently-sounding pitch from the latest inference.
|
|
struct ActiveNote
|
|
{
|
|
int midi = -1;
|
|
float confidence = 0.0f; // note posteriorgram, 0..1
|
|
// Milliseconds since this pitch's most recent detected onset, measured
|
|
// when getActiveNotes()/getDominantNote() is called. Lets the caller
|
|
// back-date a detection to the true onset rather than poll time.
|
|
// >= 1e6 means no recent onset (sustained / decaying tail).
|
|
float onsetAgeMs = 1.0e9f;
|
|
// Monotonic per-pitch onset counter — increments once per distinct
|
|
// detected onset. A change since the last poll means a NEW note was
|
|
// struck on this pitch, letting the caller treat onsets as discrete
|
|
// events instead of polling "is this pitch active".
|
|
int onsetSeq = 0;
|
|
};
|
|
|
|
// Pitches that are sounding now — either sustained above the activity
|
|
// threshold or freshly onset within the last ~200 ms. Polled from the
|
|
// main (N-API) thread.
|
|
std::vector<ActiveNote> getActiveNotes() const;
|
|
|
|
// Highest-confidence active pitch; midi < 0 when nothing is sounding.
|
|
ActiveNote getDominantNote() const;
|
|
|
|
// Whether a specific MIDI pitch is currently active. Optionally returns the
|
|
// pitch's confidence. Used by the ML-backed scoreChord path.
|
|
bool isPitchActive(int midi, float* confidenceOut = nullptr) const;
|
|
|
|
private:
|
|
struct Impl;
|
|
std::unique_ptr<Impl> impl;
|
|
std::atomic<bool> modelLoaded{false};
|
|
|
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(MlNoteDetector)
|
|
};
|