new analyzer
This commit is contained in:
1 parent
4b1fc0835a
commit
98d9fa24cf
110 files changed
+1749
-52
No files matched your search
@@ -0,0 +1,73 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Analizza i CSV prodotti da capture-features.ps1.
|
||||
|
||||
.DESCRIPTION
|
||||
Per ogni file riporta:
|
||||
- distribuzione delle etichette emesse;
|
||||
- media delle feature NORMALIZZATE (cio' che il classificatore vede davvero);
|
||||
- media dei punteggi grezzi per classe e il divario rispetto alla vincente.
|
||||
|
||||
Serve a rispondere a "perche' sbaglia su questo genere" invece di
|
||||
ritoccare i pesi a caso.
|
||||
|
||||
.PARAMETER Csv
|
||||
Uno o piu' file CSV. Il nome del file viene usato come etichetta.
|
||||
|
||||
.PARAMETER SkipSeconds
|
||||
Righe iniziali da scartare (warm-up del folding ritmico).
|
||||
|
||||
.EXAMPLE
|
||||
powershell -ExecutionPolicy Bypass -File tools\analyze-features.ps1 -Csv C:\temp\rock.csv,C:\temp\hiphop.csv
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string[]]$Csv,
|
||||
[int]$SkipSeconds = 16
|
||||
)
|
||||
|
||||
$featureNames = @(
|
||||
"dembow", "fourFloor", "tempo(latin)", "lowRatio", "flatness",
|
||||
"highRatio", "density", "centroid", "regularity", "regMid", "dynamics"
|
||||
)
|
||||
$classCols = @("s_latin", "s_electronic", "s_hiphop", "s_rock", "s_classical")
|
||||
|
||||
foreach ($path in $Csv) {
|
||||
if (-not (Test-Path $path)) { Write-Host "manca: $path" -ForegroundColor Yellow; continue }
|
||||
$all = @(Import-Csv $path)
|
||||
$rows = @($all | Select-Object -Skip $SkipSeconds)
|
||||
if ($rows.Count -eq 0) { Write-Host "nessuna riga utile in $path" -ForegroundColor Yellow; continue }
|
||||
|
||||
Write-Host ""
|
||||
Write-Host ("=== {0} ({1} righe utili) ===" -f (Split-Path -Leaf $path), $rows.Count) -ForegroundColor Cyan
|
||||
|
||||
Write-Host "etichette:"
|
||||
$rows | Group-Object label | Sort-Object Count -Descending |
|
||||
ForEach-Object { Write-Host (" {0,-11} {1,3} ({2:P0})" -f $_.Name, $_.Count, ($_.Count / $rows.Count)) }
|
||||
|
||||
Write-Host "feature normalizzate (media):"
|
||||
for ($i = 0; $i -lt $featureNames.Count; $i++) {
|
||||
$col = "f{0}_*" -f $i
|
||||
$name = ($rows[0].PSObject.Properties.Name | Where-Object { $_ -like $col } | Select-Object -First 1)
|
||||
if (-not $name) { continue }
|
||||
$avg = ($rows | Measure-Object -Property $name -Average).Average
|
||||
$bar = "#" * [int]([math]::Round($avg * 30))
|
||||
Write-Host (" {0,-14} {1,5:F2} {2}" -f $featureNames[$i], $avg, $bar)
|
||||
}
|
||||
|
||||
Write-Host "punteggi grezzi (media):"
|
||||
$scores = @{}
|
||||
foreach ($c in $classCols) {
|
||||
$scores[$c] = ($rows | Measure-Object -Property $c -Average).Average
|
||||
}
|
||||
$best = ($scores.GetEnumerator() | Sort-Object Value -Descending | Select-Object -First 1)
|
||||
foreach ($c in $classCols) {
|
||||
$v = $scores[$c]
|
||||
$gap = $v - $best.Value
|
||||
$color = if ($c -eq $best.Key) { "Green" } else { "Gray" }
|
||||
Write-Host (" {0,-14} {1,5:F2} ({2,6:F2})" -f $c.Substring(2), $v, $gap) -ForegroundColor $color
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Nota: 'dynamics' alta premia classical; 'flatness' bassa penalizza rock;"
|
||||
Write-Host " 'tempo(latin)' e' zero fuori da 85-105 BPM."
|
||||
@@ -0,0 +1,68 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Registra le feature del classificatore di genere su audio reale.
|
||||
|
||||
.DESCRIPTION
|
||||
Avvia bin\LoopbackCapture.exe con --dump-features, lascia girare la cattura
|
||||
per N secondi (durante i quali riproduci i brani da analizzare) e chiude.
|
||||
|
||||
Registra una riga al secondo con feature grezze, feature normalizzate e
|
||||
punteggi grezzi di ogni classe. Serve alla taratura: senza questi numeri
|
||||
ogni modifica alle costanti e' un'ipotesi.
|
||||
|
||||
.PARAMETER Out
|
||||
File CSV di destinazione. Un file per genere e' la scelta piu' comoda:
|
||||
rock.csv, hiphop.csv, ...
|
||||
|
||||
.PARAMETER Seconds
|
||||
Durata della cattura. Lascia almeno 20 s per brano: il primo 10-15% e'
|
||||
warm-up (4 battute) e viene scartato in analisi.
|
||||
|
||||
.PARAMETER Port
|
||||
Porta di lavoro. Non usare 4242 se il gioco e' in esecuzione.
|
||||
|
||||
.EXAMPLE
|
||||
powershell -ExecutionPolicy Bypass -File tools\capture-features.ps1 -Out C:\temp\rock.csv -Seconds 120
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$Out,
|
||||
[int]$Seconds = 90,
|
||||
[int]$Port = 4340
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$root = Split-Path -Parent $PSScriptRoot
|
||||
$exe = Join-Path $root "bin\LoopbackCapture.exe"
|
||||
if (-not (Test-Path $exe)) { throw "non trovato: $exe" }
|
||||
|
||||
$Out = [IO.Path]::GetFullPath($Out)
|
||||
$dir = Split-Path -Parent $Out
|
||||
if ($dir -and -not (Test-Path $dir)) { New-Item -ItemType Directory -Force -Path $dir | Out-Null }
|
||||
if (Test-Path $Out) { Remove-Item $Out -Force }
|
||||
|
||||
$proc = Start-Process -FilePath $exe -ArgumentList "$Port", "0", "--dump-features=$Out" -PassThru -WindowStyle Hidden
|
||||
Start-Sleep -Milliseconds 800
|
||||
if ($proc.HasExited) { throw "il bridge e' uscito subito (exit $($proc.ExitCode)). Controlla la porta $Port." }
|
||||
|
||||
Write-Host ""
|
||||
Write-Host " Registrazione avviata. Riproduci ORA i brani." -ForegroundColor Green
|
||||
Write-Host " Durata: $Seconds s. Finestra: $Out"
|
||||
Write-Host ""
|
||||
|
||||
$sw = [Diagnostics.Stopwatch]::StartNew()
|
||||
while ($sw.Elapsed.TotalSeconds -lt $Seconds) {
|
||||
$left = [int]($Seconds - $sw.Elapsed.TotalSeconds)
|
||||
Write-Host ("`r {0,3} s rimanenti " -f $left) -NoNewline
|
||||
Start-Sleep -Milliseconds 500
|
||||
if ($proc.HasExited) { Write-Host ""; Write-Host " il bridge e' terminato in anticipo (exit $($proc.ExitCode))" -ForegroundColor Yellow; break }
|
||||
}
|
||||
Write-Host ""
|
||||
|
||||
if (-not $proc.HasExited) { Stop-Process -Id $proc.Id -Force }
|
||||
Start-Sleep -Milliseconds 400
|
||||
|
||||
if (-not (Test-Path $Out)) { throw "nessun file prodotto: $Out" }
|
||||
$rows = (Get-Content $Out | Measure-Object -Line).Lines - 1
|
||||
Write-Host (" Righe raccolte: {0}" -f $rows) -ForegroundColor Green
|
||||
Write-Host (" File: {0}" -f $Out)
|
||||
if ($rows -lt 10) { Write-Host " Poche righe: l'audio era fermo o troppo basso?" -ForegroundColor Yellow }
|
||||
@@ -0,0 +1,273 @@
|
||||
<#
|
||||
.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)
|
||||
Reference in new issue
Block a user