Files
lddc/tools/genre-regression.ps1
T
2026-11-04 14:13:04 +01:00

274 lines
10 KiB
PowerShell

<#
.SYNOPSIS
Regressione del classificatore di genere su pattern sintetici.
.DESCRIPTION
Genera pattern ritmici/timbrici noti, li riproduce sull'uscita audio di default
e raccoglie le etichette GENR emesse da bin\LoopbackCapture.exe.
Serve a verificare che una modifica alle costanti del classificatore
(bin\LoopbackCapture.cs, blocco TUNING TABLE) non abbia rotto il
riconoscimento dei casi elementari. NON sostituisce la taratura su musica
reale etichettata: i timbri sintetici sono grezzi e le confidenze non sono
confrontabili con quelle su brani veri.
.PARAMETER Seconds
Durata della riproduzione per ogni pattern. Deve coprire almeno 4 battute
al tempo piu' lento (70 BPM = 13,7 s) piu' il tempo di aggancio del BPM.
.PARAMETER Port
Porta UDP di lavoro. Non usare 4242 se il gioco e' in esecuzione.
.EXAMPLE
powershell -ExecutionPolicy Bypass -File tools\genre-regression.ps1
#>
param(
[int]$Seconds = 24,
[int]$Port = 4316,
[string]$Only = ""
)
$ErrorActionPreference = "Stop"
$root = Split-Path -Parent $PSScriptRoot
$exe = Join-Path $root "bin\LoopbackCapture.exe"
if (-not (Test-Path $exe)) { throw "non trovato: $exe" }
$work = Join-Path $env:TEMP "ldcc-patterns"
New-Item -ItemType Directory -Force -Path $work | Out-Null
# --- Sintesi ---------------------------------------------------------------
# C# 5: Add-Type su Windows PowerShell 5.1 non accetta sintassi moderna.
if (-not ("LdccSynth" -as [type])) {
Add-Type -TypeDefinition @'
using System; using System.IO; using System.Text;
public static class LdccSynth
{
const int SR = 44100;
public static void Build(string path, string kind, double bpm, double seconds)
{
int n = (int)(SR * seconds);
short[] buf = new short[n];
double sixteenth = 60.0 / bpm / 4.0;
var rnd = new Random(7);
int[] kick, snare, hat;
bool chord = false, tone = false;
switch (kind)
{
case "dembow": kick = new int[] { 0, 8 }; snare = new int[] { 0,3,6,8,11,14 }; hat = new int[0]; break;
case "fourfloor": kick = new int[] { 0,4,8,12 }; snare = new int[0]; hat = new int[] { 2,6,10,14 }; break;
case "backbeat": kick = new int[] { 0,6,8 }; snare = new int[] { 4,12 }; hat = new int[] { 0,2,4,6,8,10,12,14 }; chord = true; break;
case "boombap": kick = new int[] { 0,6 }; snare = new int[] { 4,12 }; hat = new int[] { 2,6,10,14 }; break;
case "classical": kick = new int[0]; snare = new int[0]; hat = new int[0]; tone = true; break;
default: throw new ArgumentException("kind sconosciuto: " + kind);
}
for (int b = 0; ; b++)
{
double barStart = b * 16 * sixteenth;
if (barStart >= seconds) break;
foreach (int s in kick) AddKick(buf, barStart + s * sixteenth, 0.85);
foreach (int s in snare) AddSnare(buf, barStart + s * sixteenth, 0.45, rnd);
foreach (int s in hat) AddHat(buf, barStart + s * sixteenth, 0.18, rnd);
if (chord) AddChord(buf, barStart, sixteenth * 8, 0.30, rnd);
}
if (tone) AddTones(buf, seconds, rnd);
for (int i = 0; i < n; i++)
{
if (buf[i] > 30000) buf[i] = 30000;
if (buf[i] < -30000) buf[i] = -30000;
}
byte[] data = new byte[n * 2];
Buffer.BlockCopy(buf, 0, data, 0, data.Length);
using (var f = File.Create(path))
using (var w = new BinaryWriter(f))
{
w.Write(Encoding.ASCII.GetBytes("RIFF")); w.Write(36 + data.Length);
w.Write(Encoding.ASCII.GetBytes("WAVEfmt ")); w.Write(16);
w.Write((short)1); w.Write((short)1);
w.Write(SR); w.Write(SR * 2); w.Write((short)2); w.Write((short)16);
w.Write(Encoding.ASCII.GetBytes("data")); w.Write(data.Length);
w.Write(data);
}
}
static void Add(short[] b, int idx, double v)
{
if (idx < 0 || idx >= b.Length) return;
double s = b[idx] + v * 32000.0;
if (s > 30000) s = 30000;
if (s < -30000) s = -30000;
b[idx] = (short)s;
}
static void AddKick(short[] b, double t0, double amp)
{
int s0 = (int)(t0 * SR), len = (int)(0.35 * SR);
for (int i = 0; i < len; i++)
{
double t = (double)i / SR;
double phase = 2 * Math.PI * (55 * t + (1 - Math.Exp(-t * 40)) / 40);
Add(b, s0 + i, Math.Sin(phase) * Math.Exp(-t * 16) * amp);
}
}
static void AddSnare(short[] b, double t0, double amp, Random rnd)
{
int s0 = (int)(t0 * SR), len = (int)(0.15 * SR);
for (int i = 0; i < len; i++)
{
double t = (double)i / SR;
double noise = (rnd.NextDouble() * 2 - 1) * Math.Exp(-t * 28);
double body = Math.Sin(2 * Math.PI * 190 * t) * Math.Exp(-t * 30) * 0.4;
Add(b, s0 + i, (noise + body) * amp);
}
}
static void AddHat(short[] b, double t0, double amp, Random rnd)
{
int s0 = (int)(t0 * SR), len = (int)(0.04 * SR);
double prev = 0;
for (int i = 0; i < len; i++)
{
double t = (double)i / SR;
double x = (rnd.NextDouble() * 2 - 1);
double hp = x - prev; prev = x; // differenziatore: enfasi sugli acuti
Add(b, s0 + i, hp * Math.Exp(-t * 90) * amp);
}
}
// Accordo distorto: armoniche dense + clipping => spettro piatto, alta energia acuta.
static void AddChord(short[] b, double t0, double dur, double amp, Random rnd)
{
int s0 = (int)(t0 * SR), len = (int)(dur * SR);
double[] roots = { 110.0, 146.83, 164.81 };
for (int i = 0; i < len; i++)
{
double t = (double)i / SR;
double env = Math.Exp(-t * 3.0);
double v = 0;
foreach (double f in roots)
{
for (int h = 1; h <= 20; h++)
v += Math.Sin(2 * Math.PI * f * h * t + h) / h;
}
double pick = (i < (int)(0.02 * SR)) ? (rnd.NextDouble() * 2 - 1) * 0.5 : 0;
v = Math.Tanh(v * 6.0) * env * amp / 10.0 + pick * 0.25;
Add(b, s0 + i, v);
}
}
// Arpeggio lento con dinamica ampia: poca percussione, onset radi.
static void AddTones(short[] b, double seconds, Random rnd)
{
double[] notes = { 220.0, 261.63, 329.63, 392.0, 329.63, 261.63 };
double t = 0;
int i = 0;
while (t < seconds)
{
double f = notes[i % notes.Length];
double dur = 1.6 + rnd.NextDouble() * 1.2;
double amp = 0.12 + rnd.NextDouble() * 0.5;
int s0 = (int)(t * SR), len = (int)(dur * SR);
for (int k = 0; k < len; k++)
{
double tt = (double)k / SR;
double env = Math.Exp(-tt * 1.2) * (1 - Math.Exp(-tt * 60));
double v = (Math.Sin(2 * Math.PI * f * tt) + 0.3 * Math.Sin(4 * Math.PI * f * tt)) * env * amp;
Add(b, s0 + k, v);
}
t += dur * 0.85; i++;
}
}
}
'@ -ErrorAction Stop
}
$patterns = @(
@{ Name = "dembow-95"; Kind = "dembow" },
@{ Name = "fourfloor-126"; Kind = "fourfloor" },
@{ Name = "backbeat-140"; Kind = "backbeat" },
@{ Name = "boombap-90"; Kind = "boombap" },
@{ Name = "classical-70"; Kind = "classical" }
)
$expected = @{
"dembow-95" = "latin"
"fourfloor-126" = "electronic"
"backbeat-140" = "rock"
"boombap-90" = "hiphop"
"classical-70" = "classical"
}
$results = @()
foreach ($pat in $patterns) {
if ($Only -and $pat.Name -ne $Only) { continue }
$wav = Join-Path $work ($pat.Name + ".wav")
if ($pat.Kind -eq "classical") { [LdccSynth]::Build($wav, $pat.Kind, 70, $Seconds + 6) }
else { [LdccSynth]::Build($wav, $pat.Kind, [int]($pat.Name -split '-')[1], $Seconds) }
$csv = Join-Path $work ($pat.Name + ".csv")
if (Test-Path $csv) { Remove-Item $csv -Force }
$udp = New-Object System.Net.Sockets.UdpClient($Port)
$udp.Client.ReceiveTimeout = 400
$ep = New-Object System.Net.IPEndPoint([System.Net.IPAddress]::Any, 0)
$proc = Start-Process -FilePath $exe -ArgumentList "$Port", "0", "--dump-features=$csv" -PassThru -WindowStyle Hidden
Start-Sleep -Milliseconds 900
$player = New-Object System.Media.SoundPlayer $wav
$player.Play()
$labels = @()
$sw = [Diagnostics.Stopwatch]::StartNew()
# Raccolta limitata alla durata della riproduzione: il silenzio finale produrrebbe
# etichette "unknown" che non riguardano il pattern.
$limit = $Seconds * 1000
while ($sw.ElapsedMilliseconds -lt $limit) {
try {
$b = $udp.Receive([ref]$ep)
if ($b.Length -ge 5 -and [Text.Encoding]::ASCII.GetString($b, 0, 5) -eq 'GENR:') {
$txt = [Text.Encoding]::UTF8.GetString($b)
$parts = $txt.Split(';')
if ($parts.Length -ge 3) { $labels += [pscustomobject]@{ Label = $parts[1]; Conf = [double]$parts[2] } }
}
} catch { }
}
$udp.Close()
if (-not $proc.HasExited) { Stop-Process -Id $proc.Id -Force }
# Scarta il warm-up: tieni l'ultimo 60% dei campioni.
$skip = [int]($labels.Count * 0.4)
$steady = @($labels | Select-Object -Skip $skip)
$dominant = "--"
$meanConf = 0.0
if ($steady.Count -gt 0) {
$g = $steady | Group-Object Label | Sort-Object Count -Descending
$dominant = $g[0].Name
$meanConf = ($steady | Measure-Object Conf -Average).Average
}
$results += [pscustomobject]@{
Pattern = $pat.Name
Atteso = $expected[$pat.Name]
Dominante = $dominant
Conf = if ($meanConf) { [math]::Round($meanConf, 2) } else { 0 }
Esito = if ($dominant -eq $expected[$pat.Name]) { "OK" } else { "MISS" }
}
Write-Host ("{0,-16} atteso={1,-11} ottenuto={2,-11} conf={3}" -f $pat.Name, $expected[$pat.Name], $dominant, [math]::Round($meanConf, 2))
}
Write-Host ""
$results | Format-Table -AutoSize
$ok = @($results | Where-Object { $_.Esito -eq "OK" }).Count
Write-Host ("Risultato: {0}/{1} pattern riconosciuti" -f $ok, $results.Count)
Write-Host ("CSV in {0}" -f $work)