new analyzer
This commit is contained in:
1 parent
4b1fc0835a
commit
98d9fa24cf
110 files changed
+1749
-52
No files matched your search
+538
-7
@@ -1,6 +1,12 @@
|
||||
// Exit codes: 0 = clean exit / parent ended; 2 = no render endpoint;
|
||||
// 3 = instance already running; 4 = WASAPI initialization failed;
|
||||
// 5 = unhandled exception / persistent capture error;
|
||||
// 6 = lease expired (Godot is not responding).
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Runtime.InteropServices;
|
||||
@@ -9,14 +15,423 @@ using System.Threading;
|
||||
|
||||
namespace LoopbackCapture
|
||||
{
|
||||
// Independent analysis branch: never writes to RMS/onset/BPM detector state.
|
||||
sealed class RuleGenreAnalyzer
|
||||
{
|
||||
// TUNING TABLE: initial heuristics, NOT calibrated probabilities. Adjust here.
|
||||
const int FftSize = 2048, ShortHops = 400, RhythmHops = 1600;
|
||||
const int DynamicsSamples = 160, DynamicsHopCount = 10, MinDynamicsSamples = 40;
|
||||
const int MinBars = 4, MinPeaks = 4;
|
||||
const double FeatureAlpha = 0.05, Temperature = 0.20, ConfidenceThreshold = 0.30;
|
||||
const double BandMinHz = 100, BandMaxHz = 8000, HighHz = 2000;
|
||||
const double LowMinHz = 40, LowMaxHz = 120, MidMinHz = 200, MidMaxHz = 5000;
|
||||
const double CentroidMinHz = 500, CentroidMaxHz = 4000;
|
||||
const double FlatnessMin = 0.02, FlatnessMax = 0.50;
|
||||
const double HighRatioMin = 0.05, HighRatioMax = 0.60, LowRatioMin = 0.05, LowRatioMax = 0.50;
|
||||
const double DensityMax = 8, DynamicsMinDb = 6, DynamicsMaxDb = 24;
|
||||
const double LatinMinBpm = 85, LatinMaxBpm = 105, ElectronicMinBpm = 115, ElectronicMaxBpm = 135;
|
||||
const double HipHopMinBpm = 80, HipHopMaxBpm = 95, TempoShoulderBpm = 15;
|
||||
// Il dembow e' una firma discreta, non un contributo lineare: sopra soglia sposta
|
||||
// la decisione, sotto soglia non tocca nulla. Richiede anche affinita' di tempo
|
||||
// >= 0.5, altrimenti una sincope qualsiasi a 130 BPM verrebbe promossa a latin.
|
||||
// La soglia lavora sul dembow NORMALIZZATO (vedi DembowBaseline).
|
||||
const double DembowGate = 0.40, DembowBonus = 0.35;
|
||||
// Un istogramma uniforme correla gia' 7/16 con un template che ha 7 caselle attive
|
||||
// su 16. Senza togliere questo pavimento, qualsiasi ritmo piatto (rock con piatti
|
||||
// ovunque) ottiene ~0.44 e passa per un dembow debole.
|
||||
const double DembowBaseline = 7.0 / 16.0;
|
||||
// Il rilevatore puo' agganciare la cella 3+3+2 e riportare 4/3 del battito.
|
||||
// Si prova anche l'ipotesi 3/4 e si tiene quella che spiega meglio il pattern.
|
||||
const double FoldMargin = 0.05, MinFoldBpm = 50;
|
||||
static readonly double[] FoldFactors = { 1.0, 0.75, 1.25 };
|
||||
const double RegularityCenter = 0.50, RegularityWidth = 0.50;
|
||||
const double PeakSigma = 1, PowerEpsilon = 1e-20, RmsFloor = 1e-6, ActiveRms = 1e-5;
|
||||
const int PublishMs = 1000, StaleMs = 1000;
|
||||
// Columns: dembow, fourFloor, tempo affinity, low, flatness, high, density,
|
||||
// centroid, regularity, medium regularity, dynamics. Negative = inhibitory.
|
||||
// Each row has total absolute weight 1; Bias implements rewarded complements.
|
||||
// Scores include Bias, then stable softmax(score / Temperature).
|
||||
static readonly string[] Labels = { "latin", "electronic", "hiphop", "rock", "classical" };
|
||||
// Pesi derivati dalla distanza di Fisher misurata su dati reali etichettati
|
||||
// (tests\rock2.csv, classic.csv, hiphop.csv). Le feature sono quasi ortogonali:
|
||||
// flatness/highRatio separano classical da {rock, hiphop} ma NON rock da hiphop (d'~0.03);
|
||||
// regularity/regMid/dynamics separano rock da hiphop ma non rock da classical.
|
||||
// density non discrimina nessuna delle tre coppie (d' 0.14-0.45): peso azzerato.
|
||||
// I Bias NON sono scelti a mano: sono il valore che porta ogni classe a 0 sulla media
|
||||
// globale delle feature (bias_c = -w_c . mean). Senza questa compensazione una classe
|
||||
// diventa il "vincitore per default" e ogni ritaratura sposta il problema su un'altra.
|
||||
static readonly double[] Bias = { -0.273, -0.119, -0.197, 0.018, 0.191 };
|
||||
static readonly double[,] Weights = {
|
||||
{ 0.50, 0, 0.25, 0.25, 0, 0, 0, 0, 0, 0, 0 },
|
||||
{ 0, 0.35, 0.25, 0.15, 0, 0, 0, 0, 0.25, 0, 0 },
|
||||
{ 0, 0, 0, 0.15, 0, 0, 0, 0, 0.20, 0.35, 0.30 },
|
||||
{ 0, 0, 0, 0, 0.25, 0.15, 0, 0, -0.20, -0.25, -0.15 },
|
||||
{ 0, 0, 0, 0, -0.40, -0.45, 0, 0, 0, 0, 0.15 }
|
||||
};
|
||||
static readonly int[] DembowTemplate = { 1,0,0,1,0,0,1,0,1,0,0,1,0,0,1,0 };
|
||||
readonly int rate, hop;
|
||||
readonly double[] samples = new double[FftSize], hann = new double[FftSize];
|
||||
readonly double[] real = new double[FftSize], imag = new double[FftSize];
|
||||
readonly double[] cos = new double[FftSize / 2], sin = new double[FftSize / 2];
|
||||
readonly int[] reverse = new int[FftSize];
|
||||
readonly double[] previousPower = new double[FftSize / 2 + 1];
|
||||
readonly double[] shortOnset = new double[ShortHops], rhythm = new double[RhythmHops];
|
||||
readonly double[] envelope = new double[ShortHops], folded = new double[16];
|
||||
readonly double[] dynamics = new double[DynamicsSamples], sortedDynamics = new double[DynamicsSamples];
|
||||
readonly double[] features = new double[11], probabilities = new double[5];
|
||||
int write, filled, hopFrames, shortHead, shortCount, rhythmHead, rhythmCount;
|
||||
int dynamicsHead, dynamicsCount, dynamicsHops, dynamicsFrames;
|
||||
double dynamicsSum, recentRms, centroid, flatness, highRatio, lowRatio;
|
||||
double dembow, fourFloor, density, regularity, dynamicRange;
|
||||
bool spectrumReady, dynamicReady;
|
||||
long lastDataMs = -1, lastPublishMs;
|
||||
StreamWriter dump;
|
||||
public double DembowPhase { get; private set; }
|
||||
// Tempo effettivamente usato per il folding: puo' essere i 3/4 di quello rilevato
|
||||
// se il pattern dimostra che il detector ha agganciato la cella 3+3+2.
|
||||
public double EffectiveBpm { get; private set; }
|
||||
|
||||
public RuleGenreAnalyzer(int sampleRate, int hopSize, string dumpPath)
|
||||
{
|
||||
rate = sampleRate; hop = hopSize;
|
||||
for (int i = 0; i < FftSize; i++)
|
||||
{
|
||||
hann[i] = 0.5 - 0.5 * Math.Cos(2 * Math.PI * i / (FftSize - 1));
|
||||
int x = i, r = 0;
|
||||
for (int b = 0; b < 11; b++) { r = (r << 1) | (x & 1); x >>= 1; }
|
||||
reverse[i] = r;
|
||||
}
|
||||
for (int i = 0; i < cos.Length; i++)
|
||||
{
|
||||
cos[i] = Math.Cos(-2 * Math.PI * i / FftSize);
|
||||
sin[i] = Math.Sin(-2 * Math.PI * i / FftSize);
|
||||
}
|
||||
if (!string.IsNullOrEmpty(dumpPath))
|
||||
{
|
||||
try
|
||||
{
|
||||
dump = new StreamWriter(dumpPath, false, Encoding.UTF8);
|
||||
dump.WriteLine("ms,bpm,dembow,fourfloor,density,flatness,centroid,regularity,highRatio,lowRatio,label,conf,f0_latin_dembow,f1_fourfloor,f2_tempo_latin,f3_low,f4_flat,f5_high,f6_density,f7_centroid,f8_reg,f9_regmid,f10_dyn,s_latin,s_electronic,s_hiphop,s_rock,s_classical");
|
||||
dump.Flush();
|
||||
}
|
||||
catch (Exception ex) { DisableDump(ex); }
|
||||
}
|
||||
}
|
||||
|
||||
void DisableDump(Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine("Feature dump disabled: " + ex.Message);
|
||||
if (dump != null) { try { dump.Dispose(); } catch { } dump = null; }
|
||||
}
|
||||
|
||||
public void MarkData(long now) { lastDataMs = now; }
|
||||
|
||||
// Il folding ritmico dipende dal tempo: se il BPM cambia (nuovo brano), la storia
|
||||
// gia' registrata e' su una griglia che non corrisponde piu'. Va scartata, altrimenti
|
||||
// per fino a 16 s si pubblica un dembow/fourFloor casuale.
|
||||
public void ResetRhythm()
|
||||
{
|
||||
rhythmHead = 0;
|
||||
rhythmCount = 0;
|
||||
Array.Clear(rhythm, 0, rhythm.Length);
|
||||
dembow = 0;
|
||||
fourFloor = 0;
|
||||
DembowPhase = 0;
|
||||
}
|
||||
|
||||
// Errore di ottava del rilevatore: 85 BPM puo' uscire 170, 195 puo' uscire 97.5.
|
||||
// Riporta il tempo in [70,145) prima di applicare sia i template di tempo sia il
|
||||
// folding ritmico. Senza questa normalizzazione nessuna regola di tempo entra in
|
||||
// gioco (LatinMin 85, HipHopMin 80) e vince sempre la classe senza termine ritmico.
|
||||
public static double CanonicalTempo(double bpm)
|
||||
{
|
||||
if (bpm <= 0) return 0;
|
||||
while (bpm >= 145.0) bpm /= 2.0;
|
||||
while (bpm < 70.0) bpm *= 2.0;
|
||||
return bpm;
|
||||
}
|
||||
|
||||
public void AddSample(float value)
|
||||
{
|
||||
double v = float.IsNaN(value) || float.IsInfinity(value) ? 0 : value;
|
||||
samples[write] = v; write = (write + 1) % FftSize;
|
||||
if (filled < FftSize) filled++;
|
||||
dynamicsSum += v * v; dynamicsFrames++;
|
||||
if (++hopFrames < hop) return;
|
||||
hopFrames = 0;
|
||||
double flux = filled == FftSize ? Spectrum() : 0;
|
||||
shortOnset[shortHead] = flux; shortHead = (shortHead + 1) % ShortHops;
|
||||
shortCount = Math.Min(shortCount + 1, ShortHops);
|
||||
rhythm[rhythmHead] = flux; rhythmHead = (rhythmHead + 1) % RhythmHops;
|
||||
rhythmCount = Math.Min(rhythmCount + 1, RhythmHops);
|
||||
if (++dynamicsHops >= DynamicsHopCount)
|
||||
{
|
||||
recentRms = Math.Sqrt(dynamicsSum / Math.Max(1, dynamicsFrames));
|
||||
dynamics[dynamicsHead] = 20 * Math.Log10(Math.Max(RmsFloor, recentRms));
|
||||
dynamicsHead = (dynamicsHead + 1) % DynamicsSamples;
|
||||
dynamicsCount = Math.Min(dynamicsCount + 1, DynamicsSamples);
|
||||
dynamicsSum = 0; dynamicsFrames = 0; dynamicsHops = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Real input, radix-2 complex workspace; only nonnegative frequency bins used.
|
||||
// All FFT/window/twiddle/history arrays are preallocated and reused per hop.
|
||||
double Spectrum()
|
||||
{
|
||||
for (int i = 0; i < FftSize; i++)
|
||||
{
|
||||
real[reverse[i]] = samples[(write + i) % FftSize] * hann[i];
|
||||
imag[reverse[i]] = 0;
|
||||
}
|
||||
for (int size = 2; size <= FftSize; size <<= 1)
|
||||
{
|
||||
int half = size / 2, stride = FftSize / size;
|
||||
for (int start = 0; start < FftSize; start += size)
|
||||
for (int j = 0; j < half; j++)
|
||||
{
|
||||
int a = start + j, b = a + half, t = j * stride;
|
||||
double tr = cos[t] * real[b] - sin[t] * imag[b];
|
||||
double ti = sin[t] * real[b] + cos[t] * imag[b];
|
||||
real[b] = real[a] - tr; imag[b] = imag[a] - ti;
|
||||
real[a] += tr; imag[a] += ti;
|
||||
}
|
||||
}
|
||||
double total = 0, band = 0, weighted = 0, logs = 0, high = 0, low = 0, flux = 0;
|
||||
int bins = 0;
|
||||
for (int k = 0; k <= FftSize / 2; k++)
|
||||
{
|
||||
double hz = k * (double)rate / FftSize;
|
||||
double p = (real[k] * real[k] + imag[k] * imag[k]) / (FftSize * (double)FftSize);
|
||||
// One-sided power: DC/Nyquist occur once, other bins twice.
|
||||
if (k > 0 && k < FftSize / 2) p *= 2;
|
||||
total += p;
|
||||
if (hz >= BandMinHz && hz <= BandMaxHz)
|
||||
{ band += p; weighted += hz * p; logs += Math.Log(Math.Max(PowerEpsilon, p)); bins++; }
|
||||
if (hz > HighHz) high += p;
|
||||
if (hz >= LowMinHz && hz <= LowMaxHz) low += p;
|
||||
if (hz >= MidMinHz && hz <= MidMaxHz && spectrumReady)
|
||||
flux += Math.Max(0, p - previousPower[k]);
|
||||
previousPower[k] = p;
|
||||
}
|
||||
double smoothing = spectrumReady ? FeatureAlpha : 1;
|
||||
centroid += smoothing * ((band > PowerEpsilon ? weighted / band : 0) - centroid);
|
||||
flatness += smoothing * ((band > PowerEpsilon && bins > 0 ? Clamp(Math.Exp(logs / bins) / (band / bins)) : 0) - flatness);
|
||||
highRatio += smoothing * ((total > PowerEpsilon ? high / total : 0) - highRatio);
|
||||
lowRatio += smoothing * ((total > PowerEpsilon ? low / total : 0) - lowRatio);
|
||||
spectrumReady = true;
|
||||
return flux;
|
||||
}
|
||||
|
||||
static double Clamp(double x) { return Math.Max(0, Math.Min(1, x)); }
|
||||
static double Normalize(double x, double min, double max) { return Clamp((x - min) / (max - min)); }
|
||||
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)
|
||||
{
|
||||
double maximum = PowerEpsilon;
|
||||
for (int i = 0; i < shortCount; i++)
|
||||
{
|
||||
envelope[i] = shortOnset[(shortHead - shortCount + i + ShortHops) % ShortHops];
|
||||
maximum = Math.Max(maximum, envelope[i]);
|
||||
}
|
||||
double mean = 0, variance = 0;
|
||||
for (int i = 0; i < shortCount; i++) { envelope[i] /= maximum; mean += envelope[i]; }
|
||||
mean /= Math.Max(1, shortCount);
|
||||
for (int i = 0; i < shortCount; i++) variance += (envelope[i] - mean) * (envelope[i] - mean);
|
||||
double threshold = mean + PeakSigma * Math.Sqrt(variance / Math.Max(1, shortCount));
|
||||
int peaks = 0, lastPeak = -1, intervals = 0;
|
||||
double sum = 0, squares = 0;
|
||||
for (int i = 1; i < shortCount - 1; i++)
|
||||
if (envelope[i] > threshold && envelope[i] > envelope[i - 1] && envelope[i] >= envelope[i + 1])
|
||||
{
|
||||
peaks++;
|
||||
if (lastPeak >= 0) { double d = i - lastPeak; sum += d; squares += d * d; intervals++; }
|
||||
lastPeak = i;
|
||||
}
|
||||
// Available duration during warm-up; exactly 4 s once the 400-slot ring is full.
|
||||
density = peaks / Math.Max(0.01, shortCount / 100.0);
|
||||
regularity = peaks < MinPeaks ? 0 : 1 - Clamp(Math.Sqrt(Math.Max(0, squares / intervals - Math.Pow(sum / intervals, 2))) / (sum / intervals));
|
||||
dembow = 0; fourFloor = 0; DembowPhase = 0;
|
||||
EffectiveBpm = 0;
|
||||
if (bpm <= 0) return;
|
||||
|
||||
// Il pattern 3+3+2 crea periodicita' spurie a 4/3 e a 5/4 del battito: il
|
||||
// detector puo' agganciare una cella invece del battito (95 BPM letti 127 o 76).
|
||||
// Non lo si decide a priori: vince l'ipotesi che correla meglio col template.
|
||||
double bestD = 0, bestF = 0, bestP = 0, bestT = 0;
|
||||
bool found = false;
|
||||
for (int h = 0; h < FoldFactors.Length; h++)
|
||||
{
|
||||
double d, f, p;
|
||||
if (!Fold(bpm * FoldFactors[h], out d, out f, out p)) continue;
|
||||
if (!found || d > bestD + FoldMargin)
|
||||
{
|
||||
bestD = d; bestF = f; bestP = p; bestT = bpm * FoldFactors[h];
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
if (found) { dembow = bestD; fourFloor = bestF; DembowPhase = bestP; EffectiveBpm = bestT; }
|
||||
}
|
||||
|
||||
bool Fold(double bpm, out double dembowScore, out double fourFloorScore, out double phaseScore)
|
||||
{
|
||||
dembowScore = 0; fourFloorScore = 0; phaseScore = 0;
|
||||
if (bpm < MinFoldBpm) return false;
|
||||
double hopPerBeat = 60 / bpm * 100, barHops = hopPerBeat * 4;
|
||||
int bars = (int)Math.Floor(rhythmCount / barHops);
|
||||
if (bars < MinBars) return false;
|
||||
int count = (int)Math.Floor(bars * barHops);
|
||||
Array.Clear(folded, 0, folded.Length);
|
||||
double maximum = PowerEpsilon;
|
||||
for (int i = 0; i < count; i++) maximum = Math.Max(maximum, rhythm[(rhythmHead - count + i + RhythmHops) % RhythmHops]);
|
||||
for (int i = 0; i < count; i++)
|
||||
folded[((int)Math.Floor(i / hopPerBeat * 4)) % 16] += rhythm[(rhythmHead - count + i + RhythmHops) % RhythmHops] / maximum / bars;
|
||||
double sum = 0;
|
||||
for (int i = 0; i < 16; i++) sum += folded[i];
|
||||
if (sum <= PowerEpsilon) return false;
|
||||
for (int i = 0; i < 16; i++) folded[i] /= sum;
|
||||
for (int phase = 0; phase < 16; phase++)
|
||||
{
|
||||
double correlation = 0;
|
||||
for (int i = 0; i < 16; i++) correlation += folded[(i + phase) % 16] * DembowTemplate[i];
|
||||
// Unit-sum folded envelope, binary template: theoretical maximum dot = 1.
|
||||
if (correlation > dembowScore) { dembowScore = Clamp(correlation); phaseScore = phase / 16.0; }
|
||||
double mass = 0, weakest = 1;
|
||||
for (int q = 0; q < 4; q++)
|
||||
{ double v = folded[(phase + 4 * q) % 16]; mass += v; weakest = Math.Min(weakest, v); }
|
||||
// quarterMass * (weakest / meanQuarter): penalizes missing quarters.
|
||||
// Uniform 16 bins => .25; four equal quarter hits => 1; missing hit => 0.
|
||||
double floor = mass > 0 ? mass * Clamp(weakest / (mass / 4)) : 0;
|
||||
fourFloorScore = Math.Max(fourFloorScore, Clamp(floor));
|
||||
}
|
||||
// Rimuove il pavimento del 7/16: 0 = nessuna informazione ritmica, 1 = dembow pieno.
|
||||
dembowScore = Clamp((dembowScore - DembowBaseline) / (1 - DembowBaseline));
|
||||
return true;
|
||||
}
|
||||
|
||||
double Percentile(double fraction)
|
||||
{
|
||||
double index = (dynamicsCount - 1) * fraction;
|
||||
int lo = (int)index, hi = Math.Min(lo + 1, dynamicsCount - 1);
|
||||
return sortedDynamics[lo] + (sortedDynamics[hi] - sortedDynamics[lo]) * (index - lo);
|
||||
}
|
||||
|
||||
public void Publish(long now, double bpm, UdpClient udp, IPEndPoint destination)
|
||||
{
|
||||
if (now - lastPublishMs < PublishMs) return;
|
||||
lastPublishMs = now;
|
||||
bool fresh = lastDataMs >= 0 && now - lastDataMs <= StaleMs;
|
||||
RhythmFeatures(bpm);
|
||||
// Il termine di tempo usa il BPM che ha vinto il confronto di folding,
|
||||
// non quello grezzo del rilevatore.
|
||||
double tempo = EffectiveBpm > 0 ? EffectiveBpm : bpm;
|
||||
dynamicReady = dynamicsCount >= MinDynamicsSamples;
|
||||
dynamicRange = 0;
|
||||
if (dynamicReady)
|
||||
{
|
||||
Array.Copy(dynamics, sortedDynamics, dynamicsCount);
|
||||
Array.Sort(sortedDynamics, 0, dynamicsCount);
|
||||
dynamicRange = Percentile(0.90) - Percentile(0.10);
|
||||
}
|
||||
features[0] = dembow; features[1] = fourFloor;
|
||||
features[3] = Normalize(lowRatio, LowRatioMin, LowRatioMax);
|
||||
features[4] = Normalize(flatness, FlatnessMin, FlatnessMax);
|
||||
features[5] = Normalize(highRatio, HighRatioMin, HighRatioMax);
|
||||
features[6] = Normalize(density, 0, DensityMax);
|
||||
features[7] = Normalize(centroid, CentroidMinHz, CentroidMaxHz);
|
||||
features[8] = regularity;
|
||||
features[9] = Clamp(1 - Math.Abs(regularity - RegularityCenter) / RegularityWidth);
|
||||
features[10] = dynamicReady ? Normalize(dynamicRange, DynamicsMinDb, DynamicsMaxDb) : 0;
|
||||
double maxScore = double.NegativeInfinity;
|
||||
double[] rawScores = new double[Labels.Length];
|
||||
for (int c = 0; c < Labels.Length; c++)
|
||||
{
|
||||
features[2] = c == 0 ? Tempo(tempo, LatinMinBpm, LatinMaxBpm) : c == 1 ? Tempo(tempo, ElectronicMinBpm, ElectronicMaxBpm) : c == 2 ? Tempo(tempo, HipHopMinBpm, HipHopMaxBpm) : 0;
|
||||
double score = Bias[c];
|
||||
for (int j = 0; j < features.Length; j++) score += features[j] * Weights[c, j];
|
||||
// Dembow come soglia, non come peso: un pattern 3+3+2 riconosciuto vale
|
||||
// piu' di un contributo lineare, ma solo se il tempo e' compatibile.
|
||||
if (c == 0 && dembow >= DembowGate && Tempo(tempo, LatinMinBpm, LatinMaxBpm) >= 0.5)
|
||||
score += DembowBonus;
|
||||
probabilities[c] = score;
|
||||
rawScores[c] = score;
|
||||
maxScore = Math.Max(maxScore, score);
|
||||
}
|
||||
double total = 0;
|
||||
bool active = fresh && recentRms >= ActiveRms && shortCount >= ShortHops;
|
||||
for (int c = 0; c < Labels.Length; c++)
|
||||
{ probabilities[c] = active ? Math.Exp((probabilities[c] - maxScore) / Temperature) : 1; total += probabilities[c]; }
|
||||
for (int c = 0; c < Labels.Length; c++)
|
||||
probabilities[c] /= total;
|
||||
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];
|
||||
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]);
|
||||
try { byte[] bytes = Encoding.UTF8.GetBytes(packet.ToString()); udp.Send(bytes, bytes.Length, destination); } catch { }
|
||||
if (dump != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
StringBuilder row = new StringBuilder();
|
||||
row.AppendFormat(CultureInfo.InvariantCulture,
|
||||
"{0},{1:F3},{2:F3},{3:F3},{4:F3},{5:F6},{6:F3},{7:F3},{8:F6},{9:F6},{10},{11:F3}",
|
||||
now, tempo, fresh ? dembow : 0, fresh ? fourFloor : 0, fresh ? density : 0,
|
||||
fresh ? flatness : 0, fresh ? centroid : 0, fresh ? regularity : 0,
|
||||
fresh ? highRatio : 0, fresh ? lowRatio : 0, label, probabilities[best]);
|
||||
// Valori normalizzati visti dal classificatore + punteggi grezzi per classe:
|
||||
// servono a capire QUALE termine fa perdere una classe, non solo che perde.
|
||||
for (int j = 0; j < features.Length; j++) row.AppendFormat(CultureInfo.InvariantCulture, ",{0:F3}", features[j]);
|
||||
for (int c = 0; c < Labels.Length; c++) row.AppendFormat(CultureInfo.InvariantCulture, ",{0:F3}", rawScores[c]);
|
||||
dump.WriteLine(row.ToString());
|
||||
dump.Flush(); // Once a second, never on the per-frame path.
|
||||
}
|
||||
catch (Exception ex) { DisableDump(ex); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Program
|
||||
{
|
||||
const int SIO_UDP_CONNRESET = -1744830452;
|
||||
|
||||
static int selectedTargetPid = 0; // 0 = Tutto il sistema
|
||||
static Mutex instanceMutex;
|
||||
static ManualResetEvent cmdReady = new ManualResetEvent(false);
|
||||
static long leaseDeadlineMs;
|
||||
|
||||
static void Main(string[] args)
|
||||
{
|
||||
try
|
||||
{
|
||||
Run(args);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine(ex);
|
||||
Environment.Exit(5);
|
||||
}
|
||||
}
|
||||
|
||||
static void Run(string[] args)
|
||||
{
|
||||
Stopwatch lifecycleClock = Stopwatch.StartNew();
|
||||
int leaseMs = 0;
|
||||
string dumpPath = null;
|
||||
foreach (string arg in args)
|
||||
{
|
||||
if (arg.StartsWith("--dump-features=", StringComparison.Ordinal))
|
||||
dumpPath = arg.Substring(16);
|
||||
if (arg.StartsWith("--lease-ms=", StringComparison.Ordinal))
|
||||
{
|
||||
int parsedLease;
|
||||
if (int.TryParse(arg.Substring(11), out parsedLease) && parsedLease > 0)
|
||||
leaseMs = parsedLease;
|
||||
}
|
||||
}
|
||||
Interlocked.Exchange(ref leaseDeadlineMs, lifecycleClock.ElapsedMilliseconds + leaseMs);
|
||||
int port = 4242;
|
||||
int cmdPort = 4243;
|
||||
if (args.Length > 0)
|
||||
@@ -25,6 +440,35 @@ namespace LoopbackCapture
|
||||
cmdPort = port + 1;
|
||||
}
|
||||
|
||||
int parentPid = 0;
|
||||
if (args.Length > 1) int.TryParse(args[1], out parentPid);
|
||||
if (parentPid > 0)
|
||||
{
|
||||
Thread parentWatchdog = new Thread(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
using (Process parent = Process.GetProcessById(parentPid))
|
||||
{
|
||||
parent.WaitForExit();
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
Environment.Exit(0);
|
||||
});
|
||||
parentWatchdog.IsBackground = true;
|
||||
parentWatchdog.Start();
|
||||
}
|
||||
|
||||
bool createdNew;
|
||||
instanceMutex = new Mutex(false, "Local\\LoopbackCapture_" + port, out createdNew);
|
||||
if (!createdNew)
|
||||
{
|
||||
Console.Error.WriteLine("LoopbackCapture: istanza gia attiva sulla porta " + port);
|
||||
Environment.Exit(3);
|
||||
}
|
||||
GC.KeepAlive(instanceMutex);
|
||||
|
||||
UdpClient udpSend = null;
|
||||
try
|
||||
{
|
||||
@@ -44,6 +488,7 @@ namespace LoopbackCapture
|
||||
try
|
||||
{
|
||||
udpCmd = new UdpClient(cmdPort);
|
||||
cmdReady.Set();
|
||||
IPEndPoint sender = new IPEndPoint(IPAddress.Any, 0);
|
||||
while (true)
|
||||
{
|
||||
@@ -57,6 +502,11 @@ namespace LoopbackCapture
|
||||
selectedTargetPid = pid;
|
||||
}
|
||||
}
|
||||
else if (cmd.StartsWith("PING:", StringComparison.Ordinal))
|
||||
{
|
||||
Interlocked.Exchange(ref leaseDeadlineMs, lifecycleClock.ElapsedMilliseconds + leaseMs);
|
||||
}
|
||||
// Unknown commands are deliberately ignored.
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
@@ -64,29 +514,42 @@ namespace LoopbackCapture
|
||||
cmdListenerThread.IsBackground = true;
|
||||
cmdListenerThread.Start();
|
||||
|
||||
// Thread.Start alone does not guarantee that the command socket is bound.
|
||||
if (!cmdReady.WaitOne(2000))
|
||||
Console.Error.WriteLine("LoopbackCapture: command socket not ready after 2000 ms; continuing.");
|
||||
|
||||
// Announce after command readiness (or its bounded diagnostic timeout).
|
||||
try
|
||||
{
|
||||
byte[] hello = Encoding.UTF8.GetBytes("HELLO:2");
|
||||
udpSend.Send(hello, hello.Length, remoteEp);
|
||||
}
|
||||
catch { }
|
||||
|
||||
IMMDeviceEnumerator enumerator = (IMMDeviceEnumerator)new MMDeviceEnumeratorComObject();
|
||||
IMMDevice device = null;
|
||||
int hr = enumerator.GetDefaultAudioEndpoint(EDataFlow.eRender, ERole.eMultimedia, out device);
|
||||
if (hr != 0 || device == null)
|
||||
{
|
||||
hr = enumerator.GetDefaultAudioEndpoint(EDataFlow.eRender, ERole.eConsole, out device);
|
||||
if (hr != 0 || device == null) return;
|
||||
if (hr != 0 || device == null) Environment.Exit(2);
|
||||
}
|
||||
|
||||
Guid iidMgr = typeof(IAudioSessionManager2).GUID;
|
||||
object oMgr;
|
||||
device.Activate(ref iidMgr, 1, IntPtr.Zero, out oMgr);
|
||||
hr = device.Activate(ref iidMgr, 1, IntPtr.Zero, out oMgr);
|
||||
if (hr != 0 || oMgr == null) Environment.Exit(4);
|
||||
IAudioSessionManager2 sessionManager = (IAudioSessionManager2)oMgr;
|
||||
|
||||
Guid iidAudioClient = typeof(IAudioClient).GUID;
|
||||
object oAudioClient;
|
||||
hr = device.Activate(ref iidAudioClient, 1, IntPtr.Zero, out oAudioClient);
|
||||
if (hr != 0 || oAudioClient == null) return;
|
||||
if (hr != 0 || oAudioClient == null) Environment.Exit(4);
|
||||
|
||||
IAudioClient audioClient = (IAudioClient)oAudioClient;
|
||||
IntPtr pMixFormat;
|
||||
hr = audioClient.GetMixFormat(out pMixFormat);
|
||||
if (hr != 0 || pMixFormat == IntPtr.Zero) return;
|
||||
if (hr != 0 || pMixFormat == IntPtr.Zero) Environment.Exit(4);
|
||||
|
||||
WAVEFORMATEX wf = (WAVEFORMATEX)Marshal.PtrToStructure(pMixFormat, typeof(WAVEFORMATEX));
|
||||
int channels = wf.nChannels;
|
||||
@@ -112,15 +575,16 @@ namespace LoopbackCapture
|
||||
Guid emptySession = Guid.Empty;
|
||||
|
||||
hr = audioClient.Initialize(AUDCLNT_SHAREMODE_SHARED, AUDCLNT_STREAMFLAGS_LOOPBACK, 10000000, 0, pMixFormat, ref emptySession);
|
||||
if (hr != 0) return;
|
||||
if (hr != 0) Environment.Exit(4);
|
||||
|
||||
Guid iidCaptureClient = typeof(IAudioCaptureClient).GUID;
|
||||
object oCaptureClient;
|
||||
hr = audioClient.GetService(ref iidCaptureClient, out oCaptureClient);
|
||||
if (hr != 0 || oCaptureClient == null) return;
|
||||
if (hr != 0 || oCaptureClient == null) Environment.Exit(4);
|
||||
|
||||
IAudioCaptureClient captureClient = (IAudioCaptureClient)oCaptureClient;
|
||||
audioClient.Start();
|
||||
hr = audioClient.Start();
|
||||
if (hr != 0) Environment.Exit(4);
|
||||
|
||||
// Filtro Passa-Basso sui Sub-Bassi (cassa e basse frequenze: 120 Hz)
|
||||
float cutoffFreq = 120.0f;
|
||||
@@ -134,6 +598,7 @@ namespace LoopbackCapture
|
||||
// Buffer circolare onset flux temporizzato ai campioni audio reali (100 Hz = ogni 10ms esatti)
|
||||
int hopSize = sampleRate / 100;
|
||||
if (hopSize < 1) hopSize = 480;
|
||||
RuleGenreAnalyzer genreAnalyzer = new RuleGenreAnalyzer(sampleRate, hopSize, dumpPath);
|
||||
const int BUFFER_HOPS = 400; // 4.0 secondi di finestra temporale
|
||||
float[] onsetBuffer = new float[BUFFER_HOPS];
|
||||
int onsetHead = 0;
|
||||
@@ -146,15 +611,50 @@ namespace LoopbackCapture
|
||||
float prevHopTotalRms = 0;
|
||||
|
||||
float detectedBpm = 0.0f;
|
||||
double foldedBpm = 0.0; // tempo canonico con cui e' stato costruito lo storico ritmico
|
||||
int candidateBpm = 0;
|
||||
int candidateStreak = 0;
|
||||
int bpmEvalCounter = 0;
|
||||
|
||||
long lastAppListBroadcast = 0;
|
||||
int myPid = Process.GetCurrentProcess().Id;
|
||||
long lastHeartbeat = -250;
|
||||
long heartbeatCounter = 0;
|
||||
long captureErrorSince = -1;
|
||||
|
||||
while (true)
|
||||
{
|
||||
if (leaseMs > 0 && lifecycleClock.ElapsedMilliseconds > Interlocked.Read(ref leaseDeadlineMs))
|
||||
{
|
||||
// Prefer clean parent termination even if its watchdog has not run yet.
|
||||
if (parentPid > 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (Process parent = Process.GetProcessById(parentPid))
|
||||
{
|
||||
if (parent.HasExited) Environment.Exit(0);
|
||||
}
|
||||
}
|
||||
catch { Environment.Exit(0); }
|
||||
}
|
||||
// A PING may have renewed the lease while checking the parent.
|
||||
if (lifecycleClock.ElapsedMilliseconds > Interlocked.Read(ref leaseDeadlineMs))
|
||||
{
|
||||
Console.Error.WriteLine("LoopbackCapture: lease expired (Godot is not responding).");
|
||||
Environment.Exit(6);
|
||||
}
|
||||
}
|
||||
if (lifecycleClock.ElapsedMilliseconds - lastHeartbeat >= 250)
|
||||
{
|
||||
lastHeartbeat = lifecycleClock.ElapsedMilliseconds;
|
||||
try
|
||||
{
|
||||
byte[] heartbeat = Encoding.UTF8.GetBytes(string.Format("HB:{0};0", heartbeatCounter++));
|
||||
udpSend.Send(heartbeat, heartbeat.Length, remoteEp);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
// 1. Lettura campioni PCM WASAPI dal loopback di sistema
|
||||
uint packetLength = 0;
|
||||
hr = captureClient.GetNextPacketSize(out packetLength);
|
||||
@@ -168,6 +668,7 @@ namespace LoopbackCapture
|
||||
|
||||
hr = captureClient.GetBuffer(out pData, out numFramesRead, out flags, out devPos, out qpcPos);
|
||||
if (hr != 0) break;
|
||||
if (numFramesRead > 0) genreAnalyzer.MarkData(lifecycleClock.ElapsedMilliseconds);
|
||||
|
||||
if ((flags & 1) == 0 && numFramesRead > 0)
|
||||
{
|
||||
@@ -200,6 +701,7 @@ namespace LoopbackCapture
|
||||
monoSample /= channels;
|
||||
}
|
||||
|
||||
genreAnalyzer.AddSample((flags & 2) != 0 ? 0f : monoSample);
|
||||
totalSumSq += (monoSample * monoSample);
|
||||
|
||||
// Filtro IIR passa-basso a doppio polo per estrarre la cassa
|
||||
@@ -243,6 +745,7 @@ namespace LoopbackCapture
|
||||
}
|
||||
else
|
||||
{
|
||||
for (uint f = 0; f < numFramesRead; f++) genreAnalyzer.AddSample(0f);
|
||||
currentTotalRms *= 0.7f;
|
||||
}
|
||||
|
||||
@@ -250,6 +753,18 @@ namespace LoopbackCapture
|
||||
hr = captureClient.GetNextPacketSize(out packetLength);
|
||||
}
|
||||
|
||||
// A full second of consecutive failed capture iterations is persistent.
|
||||
if (hr != 0)
|
||||
{
|
||||
if (captureErrorSince < 0) captureErrorSince = lifecycleClock.ElapsedMilliseconds;
|
||||
if (lifecycleClock.ElapsedMilliseconds - captureErrorSince >= 1000)
|
||||
{
|
||||
Console.Error.WriteLine("Persistent WASAPI capture error: " + hr);
|
||||
Environment.Exit(5);
|
||||
}
|
||||
}
|
||||
else captureErrorSince = -1;
|
||||
|
||||
// 2. Volume mirato se selezionata una specifica applicazione (es. Spotify/Chrome)
|
||||
float displayRms = currentTotalRms;
|
||||
if (selectedTargetPid > 0 && sessionManager != null)
|
||||
@@ -427,6 +942,22 @@ namespace LoopbackCapture
|
||||
}
|
||||
}
|
||||
|
||||
// Il folding ritmico e' valido solo sullo stesso brano. Il ring contiene envelope
|
||||
// di onset indipendenti dal tempo: il problema non e' il BPM in se', ma mediare
|
||||
// battute di brani diversi. Serve quindi un salto DISCRETO di tempo, non la deriva
|
||||
// dell'inseguimento morbido (0.94/0.06), che altrimenti azzererebbe lo storico
|
||||
// a ogni gradino e dembow resterebbe sempre a zero.
|
||||
// Il confronto usa il tempo canonico: un flip di ottava del rilevatore (170 -> 171)
|
||||
// non deve azzerare nulla, mentre un cambio reale di brano si'.
|
||||
double canonicalBpm = RuleGenreAnalyzer.CanonicalTempo(detectedBpm);
|
||||
if (detectedBpm > 50.0f && Math.Abs(canonicalBpm - foldedBpm) > 10.0)
|
||||
{
|
||||
genreAnalyzer.ResetRhythm();
|
||||
}
|
||||
foldedBpm = canonicalBpm;
|
||||
|
||||
genreAnalyzer.Publish(lifecycleClock.ElapsedMilliseconds, canonicalBpm, udpSend, remoteEp);
|
||||
|
||||
// 4. Invio lista applicazioni attive (FILTRANDO se stesso e Godot!)
|
||||
long nowTicks = DateTime.UtcNow.Ticks;
|
||||
if ((nowTicks - lastAppListBroadcast) >= (TimeSpan.TicksPerMillisecond * 1500) && sessionManager != null)
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,10 @@
|
||||
$csc = "C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe"
|
||||
if (-not (Test-Path $csc)) {
|
||||
$csc = "C:\Windows\Microsoft.NET\Framework\v4.0.30319\csc.exe"
|
||||
}
|
||||
if (-not (Test-Path $csc)) {
|
||||
Write-Error "Compilatore .NET Framework csc.exe non trovato (Framework64 / Framework)."
|
||||
exit 1
|
||||
}
|
||||
& $csc /nologo /unsafe /optimize+ /target:exe /out:bin\LoopbackCapture.exe bin\LoopbackCapture.cs
|
||||
exit $LASTEXITCODE
|
||||
Reference in new issue
Block a user