274 lines
10 KiB
PowerShell
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)
|