<# .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)