add onnx and metadata
This commit is contained in:
1 parent
98d9fa24cf
commit
5f3f915258
18 files changed
+2229
-78
No files matched your search
+484
-57
@@ -15,6 +15,145 @@ using System.Threading;
|
||||
|
||||
namespace LoopbackCapture
|
||||
{
|
||||
static class GenreSources
|
||||
{
|
||||
// Sorgente della classificazione di genere.
|
||||
// Soluzione 1: metadati del player (GSMTC) + lookup online.
|
||||
// Soluzione 2: modello ONNX locale (fase 2; se il modello manca si ricade sulle regole).
|
||||
internal const bool EnableMetadataGenre = true;
|
||||
internal const bool EnableOnnxGenre = false;
|
||||
// Ancoraggio del BPM rilevato al tempo fornito dai metadati del player.
|
||||
internal const bool EnableMetadataBpm = true;
|
||||
|
||||
static GenreMetadataProvider metadata;
|
||||
static GenreOnnxProvider onnx;
|
||||
|
||||
internal static void InitializeOnnx(int rate, string dump)
|
||||
{ InitializeOnnxIfEnabled(EnableOnnxGenre, rate, dump); }
|
||||
|
||||
static void InitializeOnnxIfEnabled(bool enabled, int rate, string dump)
|
||||
{
|
||||
if (!enabled) return;
|
||||
onnx = new GenreOnnxProvider(rate, dump);
|
||||
onnx.Start();
|
||||
}
|
||||
|
||||
internal static void AddOnnxSample(float sample) { onnx?.AddSample(sample); }
|
||||
|
||||
internal static void Start() { StartMetadata(EnableMetadataGenre || EnableMetadataBpm); }
|
||||
static void StartMetadata(bool enabled)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!enabled) return;
|
||||
// wantGenre tells the provider whether iTunes is needed at all; with
|
||||
// EnableMetadataGenre=false it only resolves the BPM (Deezer), no iTunes.
|
||||
metadata = new GenreMetadataProvider(EnableMetadataGenre);
|
||||
metadata.Start();
|
||||
}
|
||||
catch { metadata = null; }
|
||||
}
|
||||
|
||||
internal static GenrePrediction GetPrediction(out string source)
|
||||
{ return Select(EnableMetadataGenre, EnableOnnxGenre, out source); }
|
||||
|
||||
static GenrePrediction Select(bool useMetadata, bool useOnnx, out string source)
|
||||
{
|
||||
source = "rules";
|
||||
try
|
||||
{
|
||||
string label = useMetadata ? metadata?.GetLabel() : null;
|
||||
if (label != null)
|
||||
{
|
||||
int index = Array.IndexOf(GenreOnnxProvider.Labels, label);
|
||||
if (index >= 0)
|
||||
{
|
||||
var probabilities = new float[5]; probabilities[index] = 1;
|
||||
source = "metadata";
|
||||
return new GenrePrediction(probabilities, source, Environment.TickCount64 + 10000, metadata?.GetDetail());
|
||||
}
|
||||
}
|
||||
if (useOnnx)
|
||||
{
|
||||
GenrePrediction result = onnx?.GetPrediction();
|
||||
if (result != null) { source = "onnx"; return result; }
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
source = "rules";
|
||||
return null;
|
||||
}
|
||||
|
||||
// Ancoraggio BPM: inoltra al provider di metadati. Restituisce false se il flag
|
||||
// e' disabilitato, se manca uno snapshot valido o se il bpm non e' disponibile (0).
|
||||
internal static bool TryGetMetadataBpm(out float bpm)
|
||||
{ return TryGetMetadataBpm(EnableMetadataBpm, out bpm); }
|
||||
|
||||
static bool TryGetMetadataBpm(bool enabled, out float bpm)
|
||||
{
|
||||
bpm = 0;
|
||||
if (!enabled) return false;
|
||||
try { return metadata?.TryGetBpm(out bpm) ?? false; }
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
// Raw metadata BPM for the DIAG "anchor" field, independent of EnableMetadataBpm:
|
||||
// it reports what the metadata source would provide even when anchoring is off.
|
||||
static float GetMetadataBpmForDiag()
|
||||
{
|
||||
try
|
||||
{
|
||||
float bpm;
|
||||
return metadata != null && metadata.TryGetBpm(out bpm) ? bpm : 0;
|
||||
}
|
||||
catch { return 0; }
|
||||
}
|
||||
|
||||
static string SanitizeDiagTrack(string value, int maxLength)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value)) return "";
|
||||
StringBuilder text = new StringBuilder(Math.Min(value.Length, maxLength));
|
||||
foreach (char c in value)
|
||||
{
|
||||
if (text.Length >= maxLength) break;
|
||||
if (c == ';' || c == '=' || c == '\r' || c == '\n') text.Append(' ');
|
||||
else text.Append(c);
|
||||
}
|
||||
return text.ToString().Trim();
|
||||
}
|
||||
|
||||
// Assemble the DIAG: packet from per-source state. Called on the capture thread
|
||||
// every 5 s; reads only atomic string/number fields (no locks in the hot path).
|
||||
internal static string BuildDiagPacket(float detectedBpm)
|
||||
{
|
||||
string meta, itunes, deezer, track;
|
||||
if (metadata == null)
|
||||
{
|
||||
// Provider not started (both EnableMetadataGenre and EnableMetadataBpm off).
|
||||
meta = "off"; itunes = "off"; deezer = "off"; track = "";
|
||||
}
|
||||
else
|
||||
{
|
||||
meta = metadata.MetaStatus ?? "error";
|
||||
itunes = metadata.ItunesStatus ?? "off";
|
||||
deezer = metadata.DeezerStatus ?? "off";
|
||||
track = metadata.TrackText ?? "";
|
||||
}
|
||||
string onnxStatus = onnx == null ? "off" : (onnx.Status ?? "failed");
|
||||
track = SanitizeDiagTrack(track, 80);
|
||||
float anchor = GetMetadataBpmForDiag();
|
||||
return "DIAG:1" +
|
||||
";meta=" + meta +
|
||||
";track=" + track +
|
||||
";itunes=" + itunes +
|
||||
";deezer=" + deezer +
|
||||
";onnx=" + onnxStatus +
|
||||
";bpm=" + detectedBpm.ToString("F1", CultureInfo.InvariantCulture) +
|
||||
";anchor=" + anchor.ToString("F1", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Independent analysis branch: never writes to RMS/onset/BPM detector state.
|
||||
sealed class RuleGenreAnalyzer
|
||||
{
|
||||
@@ -227,8 +366,18 @@ namespace LoopbackCapture
|
||||
static double Tempo(double bpm, double min, double max)
|
||||
{ return bpm <= 0 ? 0 : Clamp(1 - Math.Max(min - bpm, Math.Max(0, bpm - max)) / TempoShoulderBpm); }
|
||||
|
||||
void RhythmFeatures(double bpm)
|
||||
static string SanitizeDetail(string value)
|
||||
{
|
||||
StringBuilder text = new StringBuilder(value.Length);
|
||||
foreach (char c in value)
|
||||
{
|
||||
if (c == ';' || c == '=' || c == '\r' || c == '\n') text.Append(' ');
|
||||
else text.Append(c);
|
||||
}
|
||||
return text.ToString().Trim();
|
||||
}
|
||||
|
||||
void RhythmFeatures(double bpm) {
|
||||
double maximum = PowerEpsilon;
|
||||
for (int i = 0; i < shortCount; i++)
|
||||
{
|
||||
@@ -367,9 +516,18 @@ namespace LoopbackCapture
|
||||
int best = 0;
|
||||
for (int c = 1; c < Labels.Length; c++) if (probabilities[c] > probabilities[best]) best = c;
|
||||
string label = probabilities[best] < ConfidenceThreshold ? "unknown" : Labels[best];
|
||||
// Rules above ALWAYS run. Override only the wire output, not their history/dump.
|
||||
string source = "rules";
|
||||
GenrePrediction preferred = null;
|
||||
try { preferred = GenreSources.GetPrediction(out source); }
|
||||
catch { source = "rules"; }
|
||||
StringBuilder packet = new StringBuilder();
|
||||
packet.AppendFormat(CultureInfo.InvariantCulture, "GENR:1;{0};{1:F3}", label, probabilities[best]);
|
||||
for (int c = 0; c < Labels.Length; c++) packet.AppendFormat(CultureInfo.InvariantCulture, ";{0}={1:F3}", Labels[c], probabilities[c]);
|
||||
packet.AppendFormat(CultureInfo.InvariantCulture, "GENR:2;{0};{1:F3};{2}", preferred?.Label ?? label, preferred != null ? preferred.Confidence : probabilities[best], source);
|
||||
// Genere specifico del catalogo: descrittivo, non partecipa alle probabilita'.
|
||||
// ';' e '=' sono delimitatori del protocollo: sostituiti prima dell'invio.
|
||||
if (preferred != null && !string.IsNullOrEmpty(preferred.Detail))
|
||||
packet.Append(";detail=").Append(SanitizeDetail(preferred.Detail));
|
||||
for (int c = 0; c < Labels.Length; c++) packet.AppendFormat(CultureInfo.InvariantCulture, ";{0}={1:F3}", Labels[c], preferred == null ? probabilities[c] : preferred.Probabilities[c]);
|
||||
try { byte[] bytes = Encoding.UTF8.GetBytes(packet.ToString()); udp.Send(bytes, bytes.Length, destination); } catch { }
|
||||
if (dump != null)
|
||||
{
|
||||
@@ -402,6 +560,202 @@ namespace LoopbackCapture
|
||||
static ManualResetEvent cmdReady = new ManualResetEvent(false);
|
||||
static long leaseDeadlineMs;
|
||||
|
||||
// Ancoraggio del BPM rilevato al tempo fornito dai metadati. Metodo puro e
|
||||
// riusabile: usato dal loop di cattura e da --check-anchor.
|
||||
// Il rilevatore puo' leggere 4/3 o 4/5 del tempo reale su pattern 3+3+2, e puo'
|
||||
// sbagliare l'ottava (2x / 1/2x). L'ottava e' decisa dall'energia percussiva e
|
||||
// quindi e' approssimativa (tolleranza 4%), mentre i rapporti 3+3+2 sono
|
||||
// deterministici (tolleranza 2%): cosi' un tempo vicino ma non derivato dai
|
||||
// metadati (es. 130 vs 4/3 * 95 = 126.7) non viene toccato.
|
||||
static int AnchorBpm(double detected, float metadataBpm)
|
||||
{
|
||||
if (metadataBpm <= 0) return (int)Math.Round(detected);
|
||||
double a = metadataBpm;
|
||||
double best = detected, bestTarget = best, bestDist = double.MaxValue;
|
||||
// (valore che il rilevatore puo' leggere, valore corretto, tolleranza)
|
||||
var relations = new (double value, double target, double tol)[]
|
||||
{
|
||||
(a, a, 0.04),
|
||||
(2 * a, 2 * a, 0.04),
|
||||
(a / 2, a / 2, 0.04),
|
||||
(a * 4.0 / 3.0, a, 0.02),
|
||||
(a * 4.0 / 5.0, a, 0.02),
|
||||
};
|
||||
foreach (var r in relations)
|
||||
{
|
||||
double rel = Math.Abs(best - r.value) / r.value;
|
||||
if (rel <= r.tol && rel < bestDist)
|
||||
{
|
||||
bestDist = rel;
|
||||
bestTarget = r.target;
|
||||
}
|
||||
}
|
||||
return (int)Math.Round(bestTarget);
|
||||
}
|
||||
|
||||
static void CheckAnchor(string spec)
|
||||
{
|
||||
string[] parts = spec.Split(',');
|
||||
if (parts.Length < 2 ||
|
||||
!float.TryParse(parts[0], NumberStyles.Float, CultureInfo.InvariantCulture, out float anchor))
|
||||
{
|
||||
Console.Error.WriteLine("--check-anchor richiede <anchor>,<detected1>,... (numeri)");
|
||||
return;
|
||||
}
|
||||
for (int i = 1; i < parts.Length; i++)
|
||||
{
|
||||
if (!float.TryParse(parts[i], NumberStyles.Float, CultureInfo.InvariantCulture, out float detected))
|
||||
{
|
||||
Console.Error.WriteLine("--check-anchor: valore non valido: " + parts[i]);
|
||||
continue;
|
||||
}
|
||||
int anchored = AnchorBpm(detected, anchor);
|
||||
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, "{0:F1} -> {1}", detected, anchored));
|
||||
}
|
||||
}
|
||||
|
||||
// Diagnostics for --check-sources: one attempt per source, readable report, exit 0.
|
||||
// Works regardless of EnableMetadataGenre/EnableOnnxGenre, like --check-anchor for
|
||||
// the pure function; the verdict reports what would win with the current config.
|
||||
static void CheckSources(string spec)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 1. Model file presence (same path the ONNX worker resolves).
|
||||
string modelPath = Path.Combine(AppContext.BaseDirectory, "genre_model.onnx");
|
||||
string jsonPath = Path.Combine(AppContext.BaseDirectory, "genre_model.json");
|
||||
bool modelExists = File.Exists(modelPath) && File.Exists(jsonPath);
|
||||
Console.WriteLine("[MODEL] genre_model.onnx trovato=" + (modelExists ? "si" : "no") +
|
||||
(modelExists ? "" : " -> ONNX non disponibile"));
|
||||
|
||||
// 2. ONNX load attempt (only if the files are present).
|
||||
bool onnxLoaded = false;
|
||||
if (!modelExists)
|
||||
{
|
||||
Console.WriteLine("[ONNX] caricamento: saltato (modello assente)");
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
var provider = new GenreOnnxProvider(44100, null);
|
||||
provider.Start();
|
||||
if (provider.WaitForInitialization(30000))
|
||||
{
|
||||
onnxLoaded = true;
|
||||
Console.WriteLine("[ONNX] caricamento: ok");
|
||||
}
|
||||
else
|
||||
{
|
||||
string reason = provider.FailureReason;
|
||||
Console.WriteLine("[ONNX] caricamento: fallito (" + (string.IsNullOrEmpty(reason) ? provider.Status : reason) + ")");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine("[ONNX] caricamento: fallito (" + ex.Message + ")");
|
||||
}
|
||||
}
|
||||
|
||||
// 3. GSMTC / test track.
|
||||
string artist, title;
|
||||
bool gsmPlaying = false;
|
||||
if (!string.IsNullOrEmpty(spec))
|
||||
{
|
||||
int sep = spec.IndexOf('|');
|
||||
if (sep < 0) { artist = spec.Trim(); title = ""; }
|
||||
else { artist = spec.Substring(0, sep).Trim(); title = spec.Substring(sep + 1).Trim(); }
|
||||
gsmPlaying = ReadGsmcForReport(out string gArtist, out string gTitle);
|
||||
string gsmText = gsmPlaying ? "si (\"" + gArtist + " - " + gTitle + "\")" : "no";
|
||||
Console.WriteLine("[GSMTC] sessione in riproduzione: " + gsmText + " [test: \"" + artist + " - " + title + "\"]");
|
||||
}
|
||||
else
|
||||
{
|
||||
gsmPlaying = ReadGsmcForReport(out artist, out title);
|
||||
if (gsmPlaying)
|
||||
Console.WriteLine("[GSMTC] sessione in riproduzione: si (\"" + artist + " - " + title + "\")");
|
||||
else
|
||||
{
|
||||
artist = "Daft Punk"; title = "Get Lucky";
|
||||
Console.WriteLine("[GSMTC] sessione in riproduzione: no -> uso test \"Daft Punk - Get Lucky\"");
|
||||
}
|
||||
}
|
||||
|
||||
if (artist.Length == 0 && title.Length == 0)
|
||||
{
|
||||
Console.WriteLine("[ITUNES] saltato: nessun artista/titolo");
|
||||
Console.WriteLine("[DEEZER] saltato: nessun artista/titolo");
|
||||
Console.WriteLine("[VERDETTO] sorgente che vincerebbe: rules (nessun brano)");
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. iTunes.
|
||||
var (label, detail, itunesState, itunesReason) = GenreMetadataProvider.LookupItunesDetailed(artist, title);
|
||||
if (itunesState == "ok")
|
||||
Console.WriteLine("[ITUNES] risposta ok, match=si, genere=\"" + (detail ?? "") + "\" -> " + label);
|
||||
else if (itunesState == "no-match")
|
||||
Console.WriteLine("[ITUNES] risposta ok, match=no -> " + itunesReason);
|
||||
else
|
||||
Console.WriteLine("[ITUNES] errore: " + itunesReason);
|
||||
|
||||
// 5. Deezer.
|
||||
var (bpm, deezerState, deezerReason) = GenreMetadataProvider.LookupDeezerDetailed(artist, title);
|
||||
if (deezerState == "ok")
|
||||
Console.WriteLine("[DEEZER] risposta ok, match=si, bpm=" + bpm.ToString("F2", CultureInfo.InvariantCulture));
|
||||
else if (deezerState == "no-bpm")
|
||||
Console.WriteLine("[DEEZER] risposta ok, match=si, bpm assente (" + deezerReason + ")");
|
||||
else if (deezerState == "no-match")
|
||||
Console.WriteLine("[DEEZER] risposta ok, match=no -> " + deezerReason);
|
||||
else
|
||||
Console.WriteLine("[DEEZER] errore: " + deezerReason);
|
||||
|
||||
// 6. Provider startup mode under the current configuration.
|
||||
bool metadataEnabled = GenreSources.EnableMetadataGenre;
|
||||
bool metadataBpmEnabled = GenreSources.EnableMetadataBpm;
|
||||
bool onnxEnabled = GenreSources.EnableOnnxGenre;
|
||||
string metaMode;
|
||||
if (!metadataEnabled && !metadataBpmEnabled)
|
||||
metaMode = "off (provider non avviato: genere e BPM disabilitati)";
|
||||
else if (!metadataEnabled)
|
||||
metaMode = "bpm-only (provider avviato per solo BPM)";
|
||||
else
|
||||
metaMode = "on (genere" + (metadataBpmEnabled ? " + BPM" : "") + ")";
|
||||
Console.WriteLine("[META] provider: " + metaMode);
|
||||
|
||||
// 7. Verdict under the current configuration.
|
||||
bool metadataWins = metadataEnabled && itunesState == "ok";
|
||||
bool onnxWins = onnxEnabled && onnxLoaded;
|
||||
string winner = metadataWins ? "metadata" : onnxWins ? "onnx" : "rules";
|
||||
var reasons = new List<string>();
|
||||
if (!metadataWins)
|
||||
{
|
||||
if (!metadataEnabled)
|
||||
reasons.Add(metadataBpmEnabled ? "metadati solo BPM (genere disabilitato)" : "metadati disabilitati");
|
||||
else if (itunesState == "no-match") reasons.Add("nessun match iTunes");
|
||||
else reasons.Add("iTunes non disponibile");
|
||||
}
|
||||
if (!onnxWins)
|
||||
{
|
||||
if (!onnxEnabled) reasons.Add("ONNX disabilitato");
|
||||
else if (!modelExists) reasons.Add("ONNX assente");
|
||||
else reasons.Add("ONNX non caricabile");
|
||||
}
|
||||
string reasonText = reasons.Count > 0 ? string.Join(", ", reasons) : "";
|
||||
Console.WriteLine("[VERDETTO] sorgente che vincerebbe: " + winner + (reasonText.Length > 0 ? " (" + reasonText + ")" : ""));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine("[VERDETTO] errore imprevisto nel check: " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
static bool ReadGsmcForReport(out string artist, out string title)
|
||||
{
|
||||
var (a, t) = GenreMetadataProvider.ReadCurrentTrack();
|
||||
artist = a; title = t;
|
||||
return a != null && t != null;
|
||||
}
|
||||
|
||||
static void Main(string[] args)
|
||||
{
|
||||
try
|
||||
@@ -420,6 +774,44 @@ namespace LoopbackCapture
|
||||
Stopwatch lifecycleClock = Stopwatch.StartNew();
|
||||
int leaseMs = 0;
|
||||
string dumpPath = null;
|
||||
string tensorDump = null, verifyWav = null;
|
||||
int verifyRate = 0;
|
||||
for (int i = 0; i < args.Length; i++)
|
||||
{
|
||||
if (args[i].StartsWith("--dump-tensor=", StringComparison.Ordinal)) tensorDump = args[i].Substring(14);
|
||||
if (args[i].StartsWith("--verify-wav=", StringComparison.Ordinal)) verifyWav = args[i].Substring(13);
|
||||
if (args[i] == "--rate")
|
||||
{
|
||||
if (++i >= args.Length || !int.TryParse(args[i], out verifyRate) || (verifyRate != 44100 && verifyRate != 48000))
|
||||
throw new ArgumentException("--rate requires 44100 or 48000 (sample reinterpretation, no resampling)");
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < args.Length; i++)
|
||||
{
|
||||
if (args[i].StartsWith("--check-anchor=", StringComparison.Ordinal))
|
||||
{
|
||||
CheckAnchor(args[i].Substring(15));
|
||||
Environment.Exit(0);
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < args.Length; i++)
|
||||
{
|
||||
if (args[i] == "--check-sources")
|
||||
{
|
||||
CheckSources(null);
|
||||
Environment.Exit(0);
|
||||
}
|
||||
if (args[i].StartsWith("--check-sources=", StringComparison.Ordinal))
|
||||
{
|
||||
CheckSources(args[i].Substring(16));
|
||||
Environment.Exit(0);
|
||||
}
|
||||
}
|
||||
bool onnxEnabled = GenreSources.EnableOnnxGenre;
|
||||
if ((tensorDump != null || verifyWav != null) && !onnxEnabled)
|
||||
throw new InvalidOperationException("ONNX tensor dump/verification rejected: EnableOnnxGenre=false; frontend not initialized");
|
||||
if (tensorDump != null && string.IsNullOrWhiteSpace(tensorDump)) throw new ArgumentException("--dump-tensor requires a file path");
|
||||
if (verifyRate != 0 && verifyWav == null) throw new ArgumentException("--rate is only valid with --verify-wav");
|
||||
foreach (string arg in args)
|
||||
{
|
||||
if (arg.StartsWith("--dump-features=", StringComparison.Ordinal))
|
||||
@@ -434,12 +826,18 @@ namespace LoopbackCapture
|
||||
Interlocked.Exchange(ref leaseDeadlineMs, lifecycleClock.ElapsedMilliseconds + leaseMs);
|
||||
int port = 4242;
|
||||
int cmdPort = 4243;
|
||||
if (args.Length > 0)
|
||||
if (args.Length > 0 && (verifyWav == null || !args[0].StartsWith("--", StringComparison.Ordinal)))
|
||||
{
|
||||
int.TryParse(args[0], out port);
|
||||
cmdPort = port + 1;
|
||||
}
|
||||
|
||||
if (verifyWav != null)
|
||||
{
|
||||
Environment.Exit(GenreOnnxProvider.VerifyWav(verifyWav, verifyRate, tensorDump, port));
|
||||
return;
|
||||
}
|
||||
|
||||
int parentPid = 0;
|
||||
if (args.Length > 1) int.TryParse(args[1], out parentPid);
|
||||
if (parentPid > 0)
|
||||
@@ -468,6 +866,7 @@ namespace LoopbackCapture
|
||||
Environment.Exit(3);
|
||||
}
|
||||
GC.KeepAlive(instanceMutex);
|
||||
try { GenreSources.Start(); } catch { }
|
||||
|
||||
UdpClient udpSend = null;
|
||||
try
|
||||
@@ -599,6 +998,7 @@ namespace LoopbackCapture
|
||||
int hopSize = sampleRate / 100;
|
||||
if (hopSize < 1) hopSize = 480;
|
||||
RuleGenreAnalyzer genreAnalyzer = new RuleGenreAnalyzer(sampleRate, hopSize, dumpPath);
|
||||
GenreSources.InitializeOnnx(sampleRate, tensorDump);
|
||||
const int BUFFER_HOPS = 400; // 4.0 secondi di finestra temporale
|
||||
float[] onsetBuffer = new float[BUFFER_HOPS];
|
||||
int onsetHead = 0;
|
||||
@@ -621,6 +1021,7 @@ namespace LoopbackCapture
|
||||
long lastHeartbeat = -250;
|
||||
long heartbeatCounter = 0;
|
||||
long captureErrorSince = -1;
|
||||
long lastDiagBroadcast = -5000;
|
||||
|
||||
while (true)
|
||||
{
|
||||
@@ -655,6 +1056,17 @@ namespace LoopbackCapture
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
// Per-source diagnostic packet, every 5 s, same socket/destination as the rest.
|
||||
if (lifecycleClock.ElapsedMilliseconds - lastDiagBroadcast >= 5000)
|
||||
{
|
||||
lastDiagBroadcast = lifecycleClock.ElapsedMilliseconds;
|
||||
try
|
||||
{
|
||||
byte[] diag = Encoding.UTF8.GetBytes(GenreSources.BuildDiagPacket(detectedBpm));
|
||||
udpSend.Send(diag, diag.Length, remoteEp);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
// 1. Lettura campioni PCM WASAPI dal loopback di sistema
|
||||
uint packetLength = 0;
|
||||
hr = captureClient.GetNextPacketSize(out packetLength);
|
||||
@@ -702,6 +1114,7 @@ namespace LoopbackCapture
|
||||
}
|
||||
|
||||
genreAnalyzer.AddSample((flags & 2) != 0 ? 0f : monoSample);
|
||||
GenreSources.AddOnnxSample((flags & 2) != 0 ? 0f : monoSample);
|
||||
totalSumSq += (monoSample * monoSample);
|
||||
|
||||
// Filtro IIR passa-basso a doppio polo per estrarre la cassa
|
||||
@@ -745,7 +1158,11 @@ namespace LoopbackCapture
|
||||
}
|
||||
else
|
||||
{
|
||||
for (uint f = 0; f < numFramesRead; f++) genreAnalyzer.AddSample(0f);
|
||||
for (uint f = 0; f < numFramesRead; f++)
|
||||
{
|
||||
genreAnalyzer.AddSample(0f);
|
||||
GenreSources.AddOnnxSample(0f);
|
||||
}
|
||||
currentTotalRms *= 0.7f;
|
||||
}
|
||||
|
||||
@@ -895,6 +1312,16 @@ namespace LoopbackCapture
|
||||
}
|
||||
}
|
||||
|
||||
// Ancoraggio ai metadati: corregge gli errori 3+3+2 (4/3 e 4/5)
|
||||
// e le ottave usando il tempo reale del player. Se nessun candidato
|
||||
// rientra in tolleranza, bestCandidateBpm resta invariato (il metadato
|
||||
// puo' essere sbagliato: remix, edit) e l'audio resta la sorgente primaria.
|
||||
float anchorBpm;
|
||||
if (GenreSources.TryGetMetadataBpm(out anchorBpm))
|
||||
{
|
||||
bestCandidateBpm = AnchorBpm(bestCandidateBpm, anchorBpm);
|
||||
}
|
||||
|
||||
if (bestCandidateScore > 0.0005)
|
||||
{
|
||||
if (detectedBpm <= 10.0f)
|
||||
@@ -1030,63 +1457,63 @@ namespace LoopbackCapture
|
||||
public enum EDataFlow { eRender, eCapture, eAll }
|
||||
public enum ERole { eConsole, eMultimedia, eCommunications }
|
||||
|
||||
[Guid("A95664D2-9614-4F35-A746-DE8DB63617E6"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
[ComImport, Guid("A95664D2-9614-4F35-A746-DE8DB63617E6"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
public interface IMMDeviceEnumerator
|
||||
{
|
||||
int EnumAudioEndpoints(EDataFlow dataFlow, int stateMask, out IntPtr devices);
|
||||
int GetDefaultAudioEndpoint(EDataFlow dataFlow, ERole role, out IMMDevice endpoint);
|
||||
int GetDevice(string id, out IMMDevice device);
|
||||
int RegisterEndpointNotificationCallback(IntPtr client);
|
||||
int UnregisterEndpointNotificationCallback(IntPtr client);
|
||||
[PreserveSig] int EnumAudioEndpoints(EDataFlow dataFlow, int stateMask, out IntPtr devices);
|
||||
[PreserveSig] int GetDefaultAudioEndpoint(EDataFlow dataFlow, ERole role, out IMMDevice endpoint);
|
||||
[PreserveSig] int GetDevice([MarshalAs(UnmanagedType.LPWStr)] string id, out IMMDevice device);
|
||||
[PreserveSig] int RegisterEndpointNotificationCallback(IntPtr client);
|
||||
[PreserveSig] int UnregisterEndpointNotificationCallback(IntPtr client);
|
||||
}
|
||||
|
||||
[Guid("D666063F-1587-4E43-81F1-B948E807363F"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
[ComImport, Guid("D666063F-1587-4E43-81F1-B948E807363F"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
public interface IMMDevice
|
||||
{
|
||||
int Activate(ref Guid id, int clsCtx, IntPtr activationParams, [MarshalAs(UnmanagedType.IUnknown)] out object interfacePointer);
|
||||
int OpenPropertyStore(int stgmAccess, out IntPtr properties);
|
||||
int GetId([MarshalAs(UnmanagedType.LPWStr)] out string id);
|
||||
int GetState(out int state);
|
||||
[PreserveSig] int Activate(ref Guid id, int clsCtx, IntPtr activationParams, [MarshalAs(UnmanagedType.IUnknown)] out object interfacePointer);
|
||||
[PreserveSig] int OpenPropertyStore(int stgmAccess, out IntPtr properties);
|
||||
[PreserveSig] int GetId([MarshalAs(UnmanagedType.LPWStr)] out string id);
|
||||
[PreserveSig] int GetState(out int state);
|
||||
}
|
||||
|
||||
[Guid("77AA99A0-1BD6-484F-8BC7-2C654C9A9B6F"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
[ComImport, Guid("77AA99A0-1BD6-484F-8BC7-2C654C9A9B6F"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
public interface IAudioSessionManager2
|
||||
{
|
||||
int GetAudioSessionControl(IntPtr sessionId, int priority, out IntPtr control);
|
||||
int GetSimpleAudioVolume(IntPtr sessionId, int priority, out IntPtr volume);
|
||||
int GetSessionEnumerator(out IAudioSessionEnumerator sessionEnum);
|
||||
[PreserveSig] int GetAudioSessionControl(IntPtr sessionId, int priority, out IntPtr control);
|
||||
[PreserveSig] int GetSimpleAudioVolume(IntPtr sessionId, int priority, out IntPtr volume);
|
||||
[PreserveSig] int GetSessionEnumerator(out IAudioSessionEnumerator sessionEnum);
|
||||
}
|
||||
|
||||
[Guid("E2F5BB11-0570-40CA-ACDD-3AA01277DEE8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
[ComImport, Guid("E2F5BB11-0570-40CA-ACDD-3AA01277DEE8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
public interface IAudioSessionEnumerator
|
||||
{
|
||||
int GetCount(out int sessionCount);
|
||||
int GetSession(int sessionIndex, [MarshalAs(UnmanagedType.IUnknown)] out object session);
|
||||
[PreserveSig] int GetCount(out int sessionCount);
|
||||
[PreserveSig] int GetSession(int sessionIndex, [MarshalAs(UnmanagedType.IUnknown)] out object session);
|
||||
}
|
||||
|
||||
[Guid("bfb7ff88-7239-4fc9-8fa2-07c950be9c6d"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
[ComImport, Guid("bfb7ff88-7239-4fc9-8fa2-07c950be9c6d"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
public interface IAudioSessionControl2
|
||||
{
|
||||
int GetState(out int state);
|
||||
int GetDisplayName(out IntPtr name);
|
||||
int SetDisplayName(string value, IntPtr eventContext);
|
||||
int GetIconPath(out IntPtr path);
|
||||
int SetIconPath(string value, IntPtr eventContext);
|
||||
int GetGroupingParam(out Guid grouping);
|
||||
int SetGroupingParam(ref Guid grouping, IntPtr eventContext);
|
||||
int RegisterAudioSessionNotification(IntPtr client);
|
||||
int UnregisterAudioSessionNotification(IntPtr client);
|
||||
int GetSessionIdentifier(out IntPtr id);
|
||||
int GetSessionInstanceIdentifier(out IntPtr id);
|
||||
int GetProcessId(out uint processId);
|
||||
int IsSystemSoundsSession();
|
||||
int SetDuckingPreference(bool optOut);
|
||||
[PreserveSig] int GetState(out int state);
|
||||
[PreserveSig] int GetDisplayName(out IntPtr name);
|
||||
[PreserveSig] int SetDisplayName([MarshalAs(UnmanagedType.LPWStr)] string value, IntPtr eventContext);
|
||||
[PreserveSig] int GetIconPath(out IntPtr path);
|
||||
[PreserveSig] int SetIconPath([MarshalAs(UnmanagedType.LPWStr)] string value, IntPtr eventContext);
|
||||
[PreserveSig] int GetGroupingParam(out Guid grouping);
|
||||
[PreserveSig] int SetGroupingParam(ref Guid grouping, IntPtr eventContext);
|
||||
[PreserveSig] int RegisterAudioSessionNotification(IntPtr client);
|
||||
[PreserveSig] int UnregisterAudioSessionNotification(IntPtr client);
|
||||
[PreserveSig] int GetSessionIdentifier(out IntPtr id);
|
||||
[PreserveSig] int GetSessionInstanceIdentifier(out IntPtr id);
|
||||
[PreserveSig] int GetProcessId(out uint processId);
|
||||
[PreserveSig] int IsSystemSoundsSession();
|
||||
[PreserveSig] int SetDuckingPreference([MarshalAs(UnmanagedType.Bool)] bool optOut);
|
||||
}
|
||||
|
||||
[Guid("C02216F6-8C67-4B5B-9D00-D008E73E0064"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
[ComImport, Guid("C02216F6-8C67-4B5B-9D00-D008E73E0064"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
public interface IAudioMeterInformation
|
||||
{
|
||||
int GetPeakValue(out float peak);
|
||||
[PreserveSig] int GetPeakValue(out float peak);
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
@@ -1110,28 +1537,28 @@ namespace LoopbackCapture
|
||||
public Guid SubFormat;
|
||||
}
|
||||
|
||||
[Guid("1CB9AD4C-DBFA-4c32-B178-C2F568A703B2"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
[ComImport, Guid("1CB9AD4C-DBFA-4c32-B178-C2F568A703B2"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
public interface IAudioClient
|
||||
{
|
||||
int Initialize(int shareMode, int streamFlags, long hnsBufferDuration, long hnsPeriodicity, IntPtr pFormat, ref Guid audioSessionGuid);
|
||||
int GetBufferSize(out uint bufferFrameCount);
|
||||
int GetStreamLatency(out long hnsLatency);
|
||||
int GetCurrentPadding(out uint numPaddingFrames);
|
||||
int IsFormatSupported(int shareMode, IntPtr pFormat, out IntPtr ppClosestMatch);
|
||||
int GetMixFormat(out IntPtr ppDeviceFormat);
|
||||
int GetDevicePeriod(out long hnsDefaultDevicePeriod, out long hnsMinimumDevicePeriod);
|
||||
int Start();
|
||||
int Stop();
|
||||
int Reset();
|
||||
int SetEventHandle(IntPtr eventHandle);
|
||||
int GetService(ref Guid interfaceId, [MarshalAs(UnmanagedType.IUnknown)] out object interfacePointer);
|
||||
[PreserveSig] int Initialize(int shareMode, int streamFlags, long hnsBufferDuration, long hnsPeriodicity, IntPtr pFormat, ref Guid audioSessionGuid);
|
||||
[PreserveSig] int GetBufferSize(out uint bufferFrameCount);
|
||||
[PreserveSig] int GetStreamLatency(out long hnsLatency);
|
||||
[PreserveSig] int GetCurrentPadding(out uint numPaddingFrames);
|
||||
[PreserveSig] int IsFormatSupported(int shareMode, IntPtr pFormat, out IntPtr ppClosestMatch);
|
||||
[PreserveSig] int GetMixFormat(out IntPtr ppDeviceFormat);
|
||||
[PreserveSig] int GetDevicePeriod(out long hnsDefaultDevicePeriod, out long hnsMinimumDevicePeriod);
|
||||
[PreserveSig] int Start();
|
||||
[PreserveSig] int Stop();
|
||||
[PreserveSig] int Reset();
|
||||
[PreserveSig] int SetEventHandle(IntPtr eventHandle);
|
||||
[PreserveSig] int GetService(ref Guid interfaceId, [MarshalAs(UnmanagedType.IUnknown)] out object interfacePointer);
|
||||
}
|
||||
|
||||
[Guid("C8ADBD64-E71E-48a0-A4DE-185C395CD317"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
[ComImport, Guid("C8ADBD64-E71E-48a0-A4DE-185C395CD317"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
public interface IAudioCaptureClient
|
||||
{
|
||||
int GetBuffer(out IntPtr dataPointer, out uint numFramesToRead, out int bufferFlags, out ulong devicePosition, out ulong qpcPosition);
|
||||
int ReleaseBuffer(uint numFramesRead);
|
||||
int GetNextPacketSize(out uint numFramesInNextPacket);
|
||||
[PreserveSig] int GetBuffer(out IntPtr dataPointer, out uint numFramesToRead, out int bufferFlags, out ulong devicePosition, out ulong qpcPosition);
|
||||
[PreserveSig] int ReleaseBuffer(uint numFramesRead);
|
||||
[PreserveSig] int GetNextPacketSize(out uint numFramesInNextPacket);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user