first commit
This commit is contained in:
commit
4b1fc0835a
14 files changed
+1133
No files matched your search
@@ -0,0 +1,4 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
@@ -0,0 +1,2 @@
|
||||
# Normalize EOL for all files that Git considers text files.
|
||||
* text=auto eol=lf
|
||||
@@ -0,0 +1,6 @@
|
||||
# Godot 4+ specific ignores
|
||||
.godot/
|
||||
/android/
|
||||
|
||||
# Build & temp logs
|
||||
*.log
|
||||
@@ -0,0 +1,606 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
namespace LoopbackCapture
|
||||
{
|
||||
class Program
|
||||
{
|
||||
const int SIO_UDP_CONNRESET = -1744830452;
|
||||
|
||||
static int selectedTargetPid = 0; // 0 = Tutto il sistema
|
||||
|
||||
static void Main(string[] args)
|
||||
{
|
||||
int port = 4242;
|
||||
int cmdPort = 4243;
|
||||
if (args.Length > 0)
|
||||
{
|
||||
int.TryParse(args[0], out port);
|
||||
cmdPort = port + 1;
|
||||
}
|
||||
|
||||
UdpClient udpSend = null;
|
||||
try
|
||||
{
|
||||
udpSend = new UdpClient();
|
||||
byte[] inValue = new byte[] { 0, 0, 0, 0 };
|
||||
byte[] outValue = new byte[] { 0, 0, 0, 0 };
|
||||
udpSend.Client.IOControl(SIO_UDP_CONNRESET, inValue, outValue);
|
||||
}
|
||||
catch { }
|
||||
|
||||
IPEndPoint remoteEp = new IPEndPoint(IPAddress.Loopback, port);
|
||||
|
||||
// Listener comandi di selezione processo inviati da Godot
|
||||
Thread cmdListenerThread = new Thread(() =>
|
||||
{
|
||||
UdpClient udpCmd = null;
|
||||
try
|
||||
{
|
||||
udpCmd = new UdpClient(cmdPort);
|
||||
IPEndPoint sender = new IPEndPoint(IPAddress.Any, 0);
|
||||
while (true)
|
||||
{
|
||||
byte[] bytes = udpCmd.Receive(ref sender);
|
||||
string cmd = Encoding.UTF8.GetString(bytes).Trim();
|
||||
if (cmd.StartsWith("PID:"))
|
||||
{
|
||||
int pid;
|
||||
if (int.TryParse(cmd.Substring(4), out pid))
|
||||
{
|
||||
selectedTargetPid = pid;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
});
|
||||
cmdListenerThread.IsBackground = true;
|
||||
cmdListenerThread.Start();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
Guid iidMgr = typeof(IAudioSessionManager2).GUID;
|
||||
object oMgr;
|
||||
device.Activate(ref iidMgr, 1, IntPtr.Zero, out oMgr);
|
||||
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;
|
||||
|
||||
IAudioClient audioClient = (IAudioClient)oAudioClient;
|
||||
IntPtr pMixFormat;
|
||||
hr = audioClient.GetMixFormat(out pMixFormat);
|
||||
if (hr != 0 || pMixFormat == IntPtr.Zero) return;
|
||||
|
||||
WAVEFORMATEX wf = (WAVEFORMATEX)Marshal.PtrToStructure(pMixFormat, typeof(WAVEFORMATEX));
|
||||
int channels = wf.nChannels;
|
||||
int bitsPerSample = wf.wBitsPerSample;
|
||||
int sampleRate = wf.nSamplesPerSec > 0 ? wf.nSamplesPerSec : 48000;
|
||||
bool isFloat = (wf.wFormatTag == 3);
|
||||
|
||||
if (wf.wFormatTag == -2)
|
||||
{
|
||||
WAVEFORMATEXTENSIBLE wfe = (WAVEFORMATEXTENSIBLE)Marshal.PtrToStructure(pMixFormat, typeof(WAVEFORMATEXTENSIBLE));
|
||||
if (wfe.SubFormat == new Guid("00000003-0000-0010-8000-00aa00389b71")) isFloat = true;
|
||||
}
|
||||
|
||||
if (bitsPerSample == 32 && !isFloat)
|
||||
{
|
||||
byte[] raw = new byte[40];
|
||||
Marshal.Copy(pMixFormat, raw, 0, 40);
|
||||
if (raw[24] == 3) isFloat = true;
|
||||
}
|
||||
|
||||
const int AUDCLNT_STREAMFLAGS_LOOPBACK = 0x00020000;
|
||||
const int AUDCLNT_SHAREMODE_SHARED = 0;
|
||||
Guid emptySession = Guid.Empty;
|
||||
|
||||
hr = audioClient.Initialize(AUDCLNT_SHAREMODE_SHARED, AUDCLNT_STREAMFLAGS_LOOPBACK, 10000000, 0, pMixFormat, ref emptySession);
|
||||
if (hr != 0) return;
|
||||
|
||||
Guid iidCaptureClient = typeof(IAudioCaptureClient).GUID;
|
||||
object oCaptureClient;
|
||||
hr = audioClient.GetService(ref iidCaptureClient, out oCaptureClient);
|
||||
if (hr != 0 || oCaptureClient == null) return;
|
||||
|
||||
IAudioCaptureClient captureClient = (IAudioCaptureClient)oCaptureClient;
|
||||
audioClient.Start();
|
||||
|
||||
// Filtro Passa-Basso sui Sub-Bassi (cassa e basse frequenze: 120 Hz)
|
||||
float cutoffFreq = 120.0f;
|
||||
float alpha = (float)(2.0 * Math.PI * cutoffFreq / sampleRate);
|
||||
alpha = Math.Max(0.003f, Math.Min(0.08f, alpha));
|
||||
float lp1 = 0f;
|
||||
float lp2 = 0f;
|
||||
|
||||
float currentTotalRms = 0f;
|
||||
|
||||
// Buffer circolare onset flux temporizzato ai campioni audio reali (100 Hz = ogni 10ms esatti)
|
||||
int hopSize = sampleRate / 100;
|
||||
if (hopSize < 1) hopSize = 480;
|
||||
const int BUFFER_HOPS = 400; // 4.0 secondi di finestra temporale
|
||||
float[] onsetBuffer = new float[BUFFER_HOPS];
|
||||
int onsetHead = 0;
|
||||
int totalHopsRecorded = 0;
|
||||
|
||||
int hopFrameAccumulator = 0;
|
||||
double hopKickSumSq = 0;
|
||||
double hopTotalSumSq = 0;
|
||||
float prevHopKickRms = 0;
|
||||
float prevHopTotalRms = 0;
|
||||
|
||||
float detectedBpm = 0.0f;
|
||||
int candidateBpm = 0;
|
||||
int candidateStreak = 0;
|
||||
int bpmEvalCounter = 0;
|
||||
|
||||
long lastAppListBroadcast = 0;
|
||||
int myPid = Process.GetCurrentProcess().Id;
|
||||
|
||||
while (true)
|
||||
{
|
||||
// 1. Lettura campioni PCM WASAPI dal loopback di sistema
|
||||
uint packetLength = 0;
|
||||
hr = captureClient.GetNextPacketSize(out packetLength);
|
||||
|
||||
while (packetLength > 0 && hr == 0)
|
||||
{
|
||||
IntPtr pData;
|
||||
uint numFramesRead;
|
||||
int flags;
|
||||
ulong devPos, qpcPos;
|
||||
|
||||
hr = captureClient.GetBuffer(out pData, out numFramesRead, out flags, out devPos, out qpcPos);
|
||||
if (hr != 0) break;
|
||||
|
||||
if ((flags & 1) == 0 && numFramesRead > 0)
|
||||
{
|
||||
double totalSumSq = 0;
|
||||
double kickSumSq = 0;
|
||||
int frames = (int)numFramesRead;
|
||||
|
||||
unsafe
|
||||
{
|
||||
byte* ptr = (byte*)pData.ToPointer();
|
||||
for (int f = 0; f < frames; f++)
|
||||
{
|
||||
float monoSample = 0f;
|
||||
if (isFloat && bitsPerSample == 32)
|
||||
{
|
||||
float* fptr = (float*)ptr + (f * channels);
|
||||
for (int c = 0; c < channels; c++) monoSample += fptr[c];
|
||||
monoSample /= channels;
|
||||
}
|
||||
else if (!isFloat && bitsPerSample == 32)
|
||||
{
|
||||
int* iptr = (int*)ptr + (f * channels);
|
||||
for (int c = 0; c < channels; c++) monoSample += (iptr[c] / 2147483648.0f);
|
||||
monoSample /= channels;
|
||||
}
|
||||
else if (bitsPerSample == 16)
|
||||
{
|
||||
short* sptr = (short*)ptr + (f * channels);
|
||||
for (int c = 0; c < channels; c++) monoSample += (sptr[c] / 32768.0f);
|
||||
monoSample /= channels;
|
||||
}
|
||||
|
||||
totalSumSq += (monoSample * monoSample);
|
||||
|
||||
// Filtro IIR passa-basso a doppio polo per estrarre la cassa
|
||||
lp1 += alpha * (monoSample - lp1);
|
||||
lp2 += alpha * (lp1 - lp2);
|
||||
kickSumSq += (lp2 * lp2);
|
||||
|
||||
hopTotalSumSq += (monoSample * monoSample);
|
||||
hopKickSumSq += (lp2 * lp2);
|
||||
hopFrameAccumulator++;
|
||||
|
||||
// Ogni 10ms esatti di campioni audio registrati dal clock DAC
|
||||
if (hopFrameAccumulator >= hopSize)
|
||||
{
|
||||
float hopKickRms = (float)Math.Sqrt(hopKickSumSq / hopSize);
|
||||
float hopTotalRms = (float)Math.Sqrt(hopTotalSumSq / hopSize);
|
||||
|
||||
float kickFlux = Math.Max(0f, hopKickRms - prevHopKickRms);
|
||||
float totalFlux = Math.Max(0f, hopTotalRms - prevHopTotalRms);
|
||||
prevHopKickRms = hopKickRms;
|
||||
prevHopTotalRms = hopTotalRms;
|
||||
|
||||
// L'onset combina la spinta del sub-basso (3.5x) e l'impatto a banda intera
|
||||
float onset = (kickFlux * 3.5f) + totalFlux;
|
||||
onsetBuffer[onsetHead] = onset;
|
||||
onsetHead = (onsetHead + 1) % BUFFER_HOPS;
|
||||
if (totalHopsRecorded < BUFFER_HOPS) totalHopsRecorded++;
|
||||
|
||||
hopFrameAccumulator = 0;
|
||||
hopKickSumSq = 0;
|
||||
hopTotalSumSq = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (frames > 0)
|
||||
{
|
||||
float packetTotalRms = (float)Math.Sqrt(totalSumSq / frames);
|
||||
currentTotalRms = (currentTotalRms * 0.25f) + (packetTotalRms * 0.75f);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
currentTotalRms *= 0.7f;
|
||||
}
|
||||
|
||||
captureClient.ReleaseBuffer(numFramesRead);
|
||||
hr = captureClient.GetNextPacketSize(out packetLength);
|
||||
}
|
||||
|
||||
// 2. Volume mirato se selezionata una specifica applicazione (es. Spotify/Chrome)
|
||||
float displayRms = currentTotalRms;
|
||||
if (selectedTargetPid > 0 && sessionManager != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
IAudioSessionEnumerator sessionEnum;
|
||||
sessionManager.GetSessionEnumerator(out sessionEnum);
|
||||
int count;
|
||||
sessionEnum.GetCount(out count);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
object oSession;
|
||||
sessionEnum.GetSession(i, out oSession);
|
||||
var session2 = (IAudioSessionControl2)oSession;
|
||||
uint pid;
|
||||
session2.GetProcessId(out pid);
|
||||
if (pid == selectedTargetPid)
|
||||
{
|
||||
var meter = (IAudioMeterInformation)oSession;
|
||||
float peak = 0;
|
||||
meter.GetPeakValue(out peak);
|
||||
displayRms = peak;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
// 3. Autocorrelazione & Comb-Filter su spettro ritmico per rilevamento esatto del BPM
|
||||
bpmEvalCounter++;
|
||||
if (bpmEvalCounter >= 10) // Ogni ~80-100ms
|
||||
{
|
||||
bpmEvalCounter = 0;
|
||||
if (totalHopsRecorded >= 120) // Almeno 1.2 secondi di audio
|
||||
{
|
||||
// Srotola il buffer circolare e trova il valore massimo per normalizzare
|
||||
float[] env = new float[totalHopsRecorded];
|
||||
float maxOnset = 0.0000001f;
|
||||
for (int i = 0; i < totalHopsRecorded; i++)
|
||||
{
|
||||
int idx = (onsetHead - totalHopsRecorded + i + BUFFER_HOPS) % BUFFER_HOPS;
|
||||
float val = onsetBuffer[idx];
|
||||
env[i] = val;
|
||||
if (val > maxOnset) maxOnset = val;
|
||||
}
|
||||
|
||||
// Se c'è un segnale ritmico (anche con volume Windows molto basso)
|
||||
if (maxOnset > 0.00002f)
|
||||
{
|
||||
// Normalizzazione: rende l'autocorrelazione totalmente indipendente dal volume di Windows!
|
||||
for (int i = 0; i < totalHopsRecorded; i++)
|
||||
{
|
||||
env[i] /= maxOnset;
|
||||
}
|
||||
|
||||
double[] scores = new double[225];
|
||||
double[] weights = new double[] { 1.0, 0.5, 0.25, 0.125 };
|
||||
|
||||
// Valuta tutti i possibili BPM da 70 a 215
|
||||
for (int bpm = 70; bpm <= 215; bpm++)
|
||||
{
|
||||
double lag = (60.0 / bpm) * 100.0; // 100.0 Hz
|
||||
double score = 0;
|
||||
for (int h = 1; h <= 4; h++)
|
||||
{
|
||||
int l = (int)Math.Round(h * lag);
|
||||
if (l >= totalHopsRecorded) break;
|
||||
double weight = weights[h - 1];
|
||||
double dot = 0;
|
||||
int count = 0;
|
||||
for (int i = l; i < totalHopsRecorded; i++)
|
||||
{
|
||||
dot += env[i] * env[i - l];
|
||||
count++;
|
||||
}
|
||||
if (count > 0) score += weight * (dot / count);
|
||||
}
|
||||
scores[bpm] = score;
|
||||
}
|
||||
|
||||
// Bias di stabilità: leggero bonus al tempo attivo per assorbire micro-fill di 0.2s
|
||||
if (detectedBpm > 50.0f)
|
||||
{
|
||||
for (int bpm = 70; bpm <= 215; bpm++)
|
||||
{
|
||||
float diff = Math.Abs(bpm - detectedBpm);
|
||||
if (diff <= 6.0f)
|
||||
{
|
||||
scores[bpm] *= (1.0 + 0.20 * (1.0 - diff / 6.0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int bestCandidateBpm = 70;
|
||||
double bestCandidateScore = 0;
|
||||
for (int bpm = 70; bpm <= 215; bpm++)
|
||||
{
|
||||
if (scores[bpm] > bestCandidateScore)
|
||||
{
|
||||
bestCandidateScore = scores[bpm];
|
||||
bestCandidateBpm = bpm;
|
||||
}
|
||||
}
|
||||
|
||||
// Risoluzione ottave: distingue brani lenti (90 BPM) da brani veloci (180-190 BPM).
|
||||
// Raddoppia solo se C'È EFFETTIVA energia percussiva al tempo veloce (rFast/rSlow >= 0.55),
|
||||
// evitando che i leggeri charleston a 1/8 di un pezzo a 90 BPM forzino il raddoppio a 180!
|
||||
if (bestCandidateBpm <= 105 && bestCandidateBpm * 2 <= 215)
|
||||
{
|
||||
int doubleBpm = bestCandidateBpm * 2;
|
||||
if (scores[doubleBpm] >= bestCandidateScore * 0.80)
|
||||
{
|
||||
int lagSlow = (int)Math.Round((60.0 / bestCandidateBpm) * 100.0);
|
||||
int lagFast = (int)Math.Round((60.0 / doubleBpm) * 100.0);
|
||||
double rSlow = 0, rFast = 0;
|
||||
int cSlow = 0, cFast = 0;
|
||||
for (int i = lagSlow; i < totalHopsRecorded; i++) { rSlow += env[i] * env[i - lagSlow]; cSlow++; }
|
||||
for (int i = lagFast; i < totalHopsRecorded; i++) { rFast += env[i] * env[i - lagFast]; cFast++; }
|
||||
if (cSlow > 0) rSlow /= cSlow;
|
||||
if (cFast > 0) rFast /= cFast;
|
||||
|
||||
if (rFast >= rSlow * 0.55)
|
||||
{
|
||||
bestCandidateBpm = doubleBpm;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (bestCandidateScore > 0.0005)
|
||||
{
|
||||
if (detectedBpm <= 10.0f)
|
||||
{
|
||||
// Primo aggancio immediato
|
||||
detectedBpm = bestCandidateBpm;
|
||||
candidateBpm = bestCandidateBpm;
|
||||
candidateStreak = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
float diff = Math.Abs(bestCandidateBpm - detectedBpm);
|
||||
if (diff <= 4.0f)
|
||||
{
|
||||
// Micro-fluttuazione naturale del brano: adatta dolcemente
|
||||
detectedBpm = (detectedBpm * 0.94f) + (bestCandidateBpm * 0.06f);
|
||||
candidateStreak = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Il candidato si discosta significativamente dal tempo attuale.
|
||||
// Per cambiare tempo (cambio canzone effettivo) deve rimanere stabile e confermato per almeno 10 cicli (~1 secondo)!
|
||||
if (Math.Abs(bestCandidateBpm - candidateBpm) <= 3)
|
||||
{
|
||||
candidateStreak++;
|
||||
if (candidateStreak >= 10)
|
||||
{
|
||||
detectedBpm = (detectedBpm * 0.4f) + (bestCandidateBpm * 0.6f);
|
||||
if (Math.Abs(detectedBpm - bestCandidateBpm) < 2.0f)
|
||||
{
|
||||
detectedBpm = bestCandidateBpm;
|
||||
candidateStreak = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
candidateBpm = bestCandidateBpm;
|
||||
candidateStreak = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Invio lista applicazioni attive (FILTRANDO se stesso e Godot!)
|
||||
long nowTicks = DateTime.UtcNow.Ticks;
|
||||
if ((nowTicks - lastAppListBroadcast) >= (TimeSpan.TicksPerMillisecond * 1500) && sessionManager != null)
|
||||
{
|
||||
lastAppListBroadcast = nowTicks;
|
||||
try
|
||||
{
|
||||
IAudioSessionEnumerator sessionEnum;
|
||||
sessionManager.GetSessionEnumerator(out sessionEnum);
|
||||
int count;
|
||||
sessionEnum.GetCount(out count);
|
||||
StringBuilder sb = new StringBuilder("APPS:0:Tutto il sistema");
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
object oSession;
|
||||
sessionEnum.GetSession(i, out oSession);
|
||||
var session2 = (IAudioSessionControl2)oSession;
|
||||
uint pid;
|
||||
session2.GetProcessId(out pid);
|
||||
|
||||
if (pid > 0 && pid != myPid)
|
||||
{
|
||||
string name = "";
|
||||
try
|
||||
{
|
||||
Process p = Process.GetProcessById((int)pid);
|
||||
name = p.ProcessName;
|
||||
string lowerName = name.ToLower();
|
||||
// FILTRA: nasconde LoopbackCapture e Godot dal menu!
|
||||
if (lowerName.Contains("loopbackcapture") || lowerName.Contains("godot")) continue;
|
||||
}
|
||||
catch { }
|
||||
|
||||
if (!string.IsNullOrEmpty(name))
|
||||
{
|
||||
sb.AppendFormat(";{0}:{1}", pid, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
byte[] appBytes = Encoding.UTF8.GetBytes(sb.ToString());
|
||||
udpSend.Send(appBytes, appBytes.Length, remoteEp);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
// 5. Invio dati audio a Godot (8 byte: RMS + BPM)
|
||||
if (udpSend != null)
|
||||
{
|
||||
byte[] payload = new byte[8];
|
||||
Array.Copy(BitConverter.GetBytes(displayRms), 0, payload, 0, 4);
|
||||
Array.Copy(BitConverter.GetBytes(detectedBpm), 0, payload, 4, 4);
|
||||
try
|
||||
{
|
||||
udpSend.Send(payload, payload.Length, remoteEp);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
Thread.Sleep(8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[ComImport]
|
||||
[Guid("BCDE0395-E52F-467C-8E3D-C4579291692E")]
|
||||
class MMDeviceEnumeratorComObject { }
|
||||
|
||||
public enum EDataFlow { eRender, eCapture, eAll }
|
||||
public enum ERole { eConsole, eMultimedia, eCommunications }
|
||||
|
||||
[Guid("A95664D2-9614-4F35-A746-DE8DB63617E6"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
public interface IMMDeviceEnumerator
|
||||
{
|
||||
int EnumAudioEndpoints(EDataFlow dataFlow, int stateMask, out IntPtr devices);
|
||||
int GetDefaultAudioEndpoint(EDataFlow dataFlow, ERole role, out IMMDevice endpoint);
|
||||
int GetDevice(string id, out IMMDevice device);
|
||||
int RegisterEndpointNotificationCallback(IntPtr client);
|
||||
int UnregisterEndpointNotificationCallback(IntPtr client);
|
||||
}
|
||||
|
||||
[Guid("D666063F-1587-4E43-81F1-B948E807363F"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
public interface IMMDevice
|
||||
{
|
||||
int Activate(ref Guid id, int clsCtx, IntPtr activationParams, [MarshalAs(UnmanagedType.IUnknown)] out object interfacePointer);
|
||||
int OpenPropertyStore(int stgmAccess, out IntPtr properties);
|
||||
int GetId([MarshalAs(UnmanagedType.LPWStr)] out string id);
|
||||
int GetState(out int state);
|
||||
}
|
||||
|
||||
[Guid("77AA99A0-1BD6-484F-8BC7-2C654C9A9B6F"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
public interface IAudioSessionManager2
|
||||
{
|
||||
int GetAudioSessionControl(IntPtr sessionId, int priority, out IntPtr control);
|
||||
int GetSimpleAudioVolume(IntPtr sessionId, int priority, out IntPtr volume);
|
||||
int GetSessionEnumerator(out IAudioSessionEnumerator sessionEnum);
|
||||
}
|
||||
|
||||
[Guid("E2F5BB11-0570-40CA-ACDD-3AA01277DEE8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
public interface IAudioSessionEnumerator
|
||||
{
|
||||
int GetCount(out int sessionCount);
|
||||
int GetSession(int sessionIndex, [MarshalAs(UnmanagedType.IUnknown)] out object session);
|
||||
}
|
||||
|
||||
[Guid("bfb7ff88-7239-4fc9-8fa2-07c950be9c6d"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
public interface IAudioSessionControl2
|
||||
{
|
||||
int GetState(out int state);
|
||||
int GetDisplayName(out IntPtr name);
|
||||
int SetDisplayName(string value, IntPtr eventContext);
|
||||
int GetIconPath(out IntPtr path);
|
||||
int SetIconPath(string value, IntPtr eventContext);
|
||||
int GetGroupingParam(out Guid grouping);
|
||||
int SetGroupingParam(ref Guid grouping, IntPtr eventContext);
|
||||
int RegisterAudioSessionNotification(IntPtr client);
|
||||
int UnregisterAudioSessionNotification(IntPtr client);
|
||||
int GetSessionIdentifier(out IntPtr id);
|
||||
int GetSessionInstanceIdentifier(out IntPtr id);
|
||||
int GetProcessId(out uint processId);
|
||||
int IsSystemSoundsSession();
|
||||
int SetDuckingPreference(bool optOut);
|
||||
}
|
||||
|
||||
[Guid("C02216F6-8C67-4B5B-9D00-D008E73E0064"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
public interface IAudioMeterInformation
|
||||
{
|
||||
int GetPeakValue(out float peak);
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
public struct WAVEFORMATEX
|
||||
{
|
||||
public short wFormatTag;
|
||||
public short nChannels;
|
||||
public int nSamplesPerSec;
|
||||
public int nAvgBytesPerSec;
|
||||
public short nBlockAlign;
|
||||
public short wBitsPerSample;
|
||||
public short cbSize;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
public struct WAVEFORMATEXTENSIBLE
|
||||
{
|
||||
public WAVEFORMATEX Format;
|
||||
public short wValidBitsPerSample;
|
||||
public int dwChannelMask;
|
||||
public Guid SubFormat;
|
||||
}
|
||||
|
||||
[Guid("1CB9AD4C-DBFA-4c32-B178-C2F568A703B2"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
public interface IAudioClient
|
||||
{
|
||||
int Initialize(int shareMode, int streamFlags, long hnsBufferDuration, long hnsPeriodicity, IntPtr pFormat, ref Guid audioSessionGuid);
|
||||
int GetBufferSize(out uint bufferFrameCount);
|
||||
int GetStreamLatency(out long hnsLatency);
|
||||
int GetCurrentPadding(out uint numPaddingFrames);
|
||||
int IsFormatSupported(int shareMode, IntPtr pFormat, out IntPtr ppClosestMatch);
|
||||
int GetMixFormat(out IntPtr ppDeviceFormat);
|
||||
int GetDevicePeriod(out long hnsDefaultDevicePeriod, out long hnsMinimumDevicePeriod);
|
||||
int Start();
|
||||
int Stop();
|
||||
int Reset();
|
||||
int SetEventHandle(IntPtr eventHandle);
|
||||
int GetService(ref Guid interfaceId, [MarshalAs(UnmanagedType.IUnknown)] out object interfacePointer);
|
||||
}
|
||||
|
||||
[Guid("C8ADBD64-E71E-48a0-A4DE-185C395CD317"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
public interface IAudioCaptureClient
|
||||
{
|
||||
int GetBuffer(out IntPtr dataPointer, out uint numFramesToRead, out int bufferFlags, out ulong devicePosition, out ulong qpcPosition);
|
||||
int ReleaseBuffer(uint numFramesRead);
|
||||
int GetNextPacketSize(out uint numFramesInNextPacket);
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,14 @@
|
||||
[gd_resource type="AudioBusLayout" format=3 uid="uid://de2evqqag5qan"]
|
||||
|
||||
[sub_resource type="AudioEffectCapture" id="AudioEffectCapture_1"]
|
||||
resource_name = "AudioEffectCapture"
|
||||
|
||||
[resource]
|
||||
bus/1/name = &"RecordBus"
|
||||
bus/1/solo = false
|
||||
bus/1/mute = true
|
||||
bus/1/bypass_fx = false
|
||||
bus/1/volume_db = 0.0
|
||||
bus/1/send = &"Master"
|
||||
bus/1/effect/0/effect = SubResource("AudioEffectCapture_1")
|
||||
bus/1/effect/0/enabled = true
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128"><rect width="124" height="124" x="2" y="2" fill="#363d52" stroke="#212532" stroke-width="4" rx="14"/><g fill="#fff" transform="translate(12.322 12.322)scale(.101)"><path d="M105 673v33q407 354 814 0v-33z"/><path fill="#478cbf" d="m105 673 152 14q12 1 15 14l4 67 132 10 8-61q2-11 15-15h162q13 4 15 15l8 61 132-10 4-67q3-13 15-14l152-14V427q30-39 56-81-35-59-83-108-43 20-82 47-40-37-88-64 7-51 8-102-59-28-123-42-26 43-46 89-49-7-98 0-20-46-46-89-64 14-123 42 1 51 8 102-48 27-88 64-39-27-82-47-48 49-83 108 26 42 56 81zm0 33v39c0 276 813 276 814 0v-39l-134 12-5 69q-2 10-14 13l-162 11q-12 0-16-11l-10-65H446l-10 65q-4 11-16 11l-162-11q-12-3-14-13l-5-69z"/><path d="M483 600c0 34 58 34 58 0v-86c0-34-58-34-58 0z"/><circle cx="725" cy="526" r="90"/><circle cx="299" cy="526" r="90"/></g><g fill="#414042" transform="translate(12.322 12.322)scale(.101)"><circle cx="307" cy="532" r="60"/><circle cx="717" cy="532" r="60"/></g></svg>
|
||||
|
After Width: | Height: | Size: 995 B |
@@ -0,0 +1,43 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://c2yvovlrnbclm"
|
||||
path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://icon.svg"
|
||||
dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=false
|
||||
editor/convert_colors_with_editor_theme=false
|
||||
@@ -0,0 +1,37 @@
|
||||
; Engine configuration file.
|
||||
; It's best edited using the editor UI and not directly,
|
||||
; since the parameters that go here are not all obvious.
|
||||
;
|
||||
; Format:
|
||||
; [section] ; section goes between []
|
||||
; param=value ; assign values to parameters
|
||||
|
||||
config_version=5
|
||||
|
||||
[application]
|
||||
|
||||
config/name="lddc"
|
||||
run/main_scene="res://scenes/Main.tscn"
|
||||
config/features=PackedStringArray("4.6", "GL Compatibility")
|
||||
config/icon="res://icon.svg"
|
||||
|
||||
[audio]
|
||||
|
||||
driver/enable_input=true
|
||||
|
||||
[display]
|
||||
|
||||
window/size/viewport_width=400
|
||||
window/size/viewport_height=500
|
||||
window/size/always_on_top=true
|
||||
window/stretch/mode="canvas_items"
|
||||
|
||||
[physics]
|
||||
|
||||
3d/physics_engine="Jolt Physics"
|
||||
|
||||
[rendering]
|
||||
|
||||
rendering_device/driver.windows="d3d12"
|
||||
renderer/rendering_method="gl_compatibility"
|
||||
renderer/rendering_method.mobile="gl_compatibility"
|
||||
@@ -0,0 +1,93 @@
|
||||
[gd_scene load_steps=5 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://scripts/SystemAudioAnalyzer.gd" id="1_audio_analyzer"]
|
||||
[ext_resource type="Script" path="res://scripts/CompanionController.gd" id="2_companion"]
|
||||
[ext_resource type="Texture2D" path="res://icon.svg" id="3_icon"]
|
||||
|
||||
[sub_resource type="SpriteFrames" id="SpriteFrames_1"]
|
||||
animations = [{
|
||||
"frames": [{
|
||||
"duration": 1.0,
|
||||
"texture": ExtResource("3_icon")
|
||||
}],
|
||||
"loop": true,
|
||||
"name": &"idle",
|
||||
"speed": 5.0
|
||||
}, {
|
||||
"frames": [{
|
||||
"duration": 1.0,
|
||||
"texture": ExtResource("3_icon")
|
||||
}],
|
||||
"loop": true,
|
||||
"name": &"dance",
|
||||
"speed": 10.0
|
||||
}, {
|
||||
"frames": [{
|
||||
"duration": 1.0,
|
||||
"texture": ExtResource("3_icon")
|
||||
}],
|
||||
"loop": true,
|
||||
"name": &"sleep",
|
||||
"speed": 2.0
|
||||
}]
|
||||
|
||||
[node name="Main" type="Node2D"]
|
||||
|
||||
[node name="SystemAudioAnalyzer" type="Node" parent="."]
|
||||
script = ExtResource("1_audio_analyzer")
|
||||
|
||||
[node name="Companion" type="Node2D" parent="." node_paths=PackedStringArray("audio_analyzer", "sprite", "debug_label", "volume_bar", "app_selector")]
|
||||
position = Vector2(200, 280)
|
||||
script = ExtResource("2_companion")
|
||||
audio_analyzer = NodePath("../SystemAudioAnalyzer")
|
||||
sprite = NodePath("AnimatedSprite2D")
|
||||
debug_label = NodePath("../UI/MarginContainer/VBoxContainer/DebugLabel")
|
||||
volume_bar = NodePath("../UI/MarginContainer/VBoxContainer/VolumeBar")
|
||||
app_selector = NodePath("../UI/MarginContainer/VBoxContainer/AppSelector")
|
||||
|
||||
[node name="AnimatedSprite2D" type="AnimatedSprite2D" parent="Companion"]
|
||||
scale = Vector2(1.2, 1.2)
|
||||
sprite_frames = SubResource("SpriteFrames_1")
|
||||
animation = &"idle"
|
||||
|
||||
[node name="UI" type="CanvasLayer" parent="."]
|
||||
|
||||
[node name="MarginContainer" type="MarginContainer" parent="UI"]
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
theme_override_constants/margin_left = 16
|
||||
theme_override_constants/margin_top = 16
|
||||
theme_override_constants/margin_right = 16
|
||||
theme_override_constants/margin_bottom = 16
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="UI/MarginContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 0
|
||||
theme_override_constants/separation = 6
|
||||
|
||||
[node name="TitleLabel" type="Label" parent="UI/MarginContainer/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.9, 0.9, 0.9, 1)
|
||||
theme_override_font_sizes/font_size = 13
|
||||
text = "Desktop Companion"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="AppSelector" type="OptionButton" parent="UI/MarginContainer/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
tooltip_text = "Seleziona l'applicazione da ascoltare (Spotify, Chrome, ecc.)"
|
||||
|
||||
[node name="VolumeBar" type="ProgressBar" parent="UI/MarginContainer/VBoxContainer"]
|
||||
custom_minimum_size = Vector2(0, 8)
|
||||
layout_mode = 2
|
||||
max_value = 1.0
|
||||
show_percentage = false
|
||||
|
||||
[node name="DebugLabel" type="Label" parent="UI/MarginContainer/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.65, 0.9, 0.75, 1)
|
||||
theme_override_font_sizes/font_size = 12
|
||||
text = "Stato: IDLE | Volume: 0.00 | Rilevamento..."
|
||||
horizontal_alignment = 1
|
||||
@@ -0,0 +1,214 @@
|
||||
class_name CompanionController
|
||||
extends Node2D
|
||||
|
||||
enum State {
|
||||
IDLE,
|
||||
DANCE,
|
||||
SLEEP
|
||||
}
|
||||
|
||||
@export_group("Audio Reactivity")
|
||||
@export_range(0.01, 1.0, 0.01) var audio_threshold: float = 0.04
|
||||
@export var dance_decay_time: float = 0.5
|
||||
@export var idle_to_sleep_time: float = 2.5
|
||||
|
||||
@export_group("Rhythm & Squish")
|
||||
@export_range(0.05, 0.5, 0.01) var squish_amount: float = 0.22
|
||||
@export var min_dance_animation_speed: float = 0.9
|
||||
@export var max_dance_animation_speed: float = 2.0
|
||||
|
||||
@export_group("Dependencies")
|
||||
@export var audio_analyzer: SystemAudioAnalyzer
|
||||
@export var sprite: AnimatedSprite2D
|
||||
@export var debug_label: Label
|
||||
@export var volume_bar: ProgressBar
|
||||
@export var app_selector: OptionButton
|
||||
|
||||
var current_state: State = State.IDLE
|
||||
var _silence_timer: float = 0.0
|
||||
var _current_rms: float = 0.0
|
||||
|
||||
# BPM dinamico: parte da 0 finché la canzone non viene agganciata
|
||||
var _current_bpm: float = 0.0
|
||||
var _target_bpm: float = 0.0
|
||||
var _base_scale: Vector2 = Vector2.ONE
|
||||
var _beat_phase: float = 0.0
|
||||
var _current_squish: float = 0.0
|
||||
var _breathe_time: float = 0.0
|
||||
|
||||
var _selected_pid: int = 0
|
||||
var _known_apps: Array[Dictionary] = []
|
||||
|
||||
func _ready() -> void:
|
||||
_base_scale = scale
|
||||
_current_bpm = 0.0
|
||||
_target_bpm = 0.0
|
||||
|
||||
if audio_analyzer:
|
||||
audio_analyzer.volume_updated.connect(_on_volume_updated)
|
||||
audio_analyzer.bpm_updated.connect(_on_bpm_updated)
|
||||
audio_analyzer.applications_updated.connect(_on_applications_updated)
|
||||
|
||||
if app_selector:
|
||||
app_selector.item_selected.connect(_on_app_selected)
|
||||
app_selector.clear()
|
||||
app_selector.add_item("Tutto il sistema", 0)
|
||||
|
||||
_set_state(State.IDLE)
|
||||
|
||||
func _on_applications_updated(apps: Array[Dictionary]) -> void:
|
||||
if not app_selector:
|
||||
return
|
||||
|
||||
# Evita di ricreare la lista se gli elementi sono identici (previene glitch della UI)
|
||||
var is_same = (apps.size() == _known_apps.size())
|
||||
if is_same:
|
||||
for i in range(apps.size()):
|
||||
if apps[i]["pid"] != _known_apps[i]["pid"] or apps[i]["name"] != _known_apps[i]["name"]:
|
||||
is_same = false
|
||||
break
|
||||
if is_same:
|
||||
return
|
||||
|
||||
_known_apps = apps
|
||||
var current_selected_pid = _selected_pid
|
||||
|
||||
app_selector.clear()
|
||||
var selected_idx = 0
|
||||
|
||||
for i in range(apps.size()):
|
||||
var app = apps[i]
|
||||
var label_text = app["name"]
|
||||
if app["pid"] == 0:
|
||||
label_text = "Tutto il sistema"
|
||||
else:
|
||||
label_text = "%s (PID %d)" % [app["name"], app["pid"]]
|
||||
|
||||
app_selector.add_item(label_text, i)
|
||||
if app["pid"] == current_selected_pid:
|
||||
selected_idx = i
|
||||
|
||||
app_selector.select(selected_idx)
|
||||
|
||||
func _on_app_selected(index: int) -> void:
|
||||
if index >= 0 and index < _known_apps.size():
|
||||
var chosen = _known_apps[index]
|
||||
_selected_pid = chosen["pid"]
|
||||
# Resetta il BPM per rilevare prontamente la nuova applicazione
|
||||
_target_bpm = 0.0
|
||||
_current_bpm = 0.0
|
||||
if audio_analyzer:
|
||||
audio_analyzer.select_application_pid(_selected_pid)
|
||||
|
||||
func _on_bpm_updated(detected_bpm: float) -> void:
|
||||
if detected_bpm > 50.0:
|
||||
_target_bpm = detected_bpm
|
||||
if _current_bpm <= 10.0:
|
||||
_current_bpm = detected_bpm
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if _target_bpm > 50.0:
|
||||
_current_bpm = lerpf(_current_bpm, _target_bpm, delta * 3.0)
|
||||
|
||||
_handle_state_logic(delta)
|
||||
_handle_rhythm_squish(delta)
|
||||
_update_debug_ui()
|
||||
|
||||
func _on_volume_updated(rms: float) -> void:
|
||||
_current_rms = lerpf(_current_rms, rms, 0.35)
|
||||
|
||||
func _handle_state_logic(delta: float) -> void:
|
||||
var is_audio_playing: bool = _current_rms >= audio_threshold
|
||||
|
||||
match current_state:
|
||||
State.DANCE:
|
||||
if is_audio_playing:
|
||||
_silence_timer = 0.0
|
||||
_scale_dance_speed(_current_rms)
|
||||
else:
|
||||
_silence_timer += delta
|
||||
if _silence_timer >= dance_decay_time:
|
||||
_set_state(State.IDLE)
|
||||
|
||||
State.IDLE:
|
||||
if is_audio_playing:
|
||||
_set_state(State.DANCE)
|
||||
else:
|
||||
_silence_timer += delta
|
||||
if _silence_timer >= idle_to_sleep_time:
|
||||
_set_state(State.SLEEP)
|
||||
|
||||
State.SLEEP:
|
||||
if is_audio_playing:
|
||||
_set_state(State.DANCE)
|
||||
|
||||
func _set_state(new_state: State) -> void:
|
||||
current_state = new_state
|
||||
_silence_timer = 0.0
|
||||
|
||||
match current_state:
|
||||
State.IDLE:
|
||||
if sprite:
|
||||
sprite.speed_scale = 1.0
|
||||
_play_animation("idle")
|
||||
State.DANCE:
|
||||
_scale_dance_speed(_current_rms)
|
||||
_play_animation("dance")
|
||||
State.SLEEP:
|
||||
if sprite:
|
||||
sprite.speed_scale = 1.0
|
||||
_play_animation("sleep")
|
||||
|
||||
func _scale_dance_speed(rms: float) -> void:
|
||||
var normalized_intensity: float = clampf((rms - audio_threshold) * 2.5, 0.0, 1.0)
|
||||
var target_speed: float = lerpf(min_dance_animation_speed, max_dance_animation_speed, normalized_intensity)
|
||||
if sprite:
|
||||
sprite.speed_scale = target_speed
|
||||
|
||||
func _play_animation(anim_name: String) -> void:
|
||||
if sprite and sprite.sprite_frames and sprite.sprite_frames.has_animation(anim_name):
|
||||
sprite.play(anim_name)
|
||||
|
||||
func _handle_rhythm_squish(delta: float) -> void:
|
||||
match current_state:
|
||||
State.DANCE:
|
||||
var effective_bpm = _current_bpm if _current_bpm > 50.0 else 130.0
|
||||
var beats_per_second: float = effective_bpm / 60.0
|
||||
_beat_phase += delta * beats_per_second * TAU
|
||||
|
||||
var raw_wave: float = -cos(_beat_phase)
|
||||
var shaped_wave: float = sign(raw_wave) * pow(abs(raw_wave), 0.75)
|
||||
|
||||
var dynamic_depth: float = squish_amount * clampf(0.8 + _current_rms * 0.8, 0.8, 1.5)
|
||||
var target_squish: float = shaped_wave * dynamic_depth
|
||||
|
||||
_current_squish = lerpf(_current_squish, target_squish, delta * 25.0)
|
||||
|
||||
scale.x = _base_scale.x * (1.0 + _current_squish * 0.85)
|
||||
scale.y = _base_scale.y * (1.0 - _current_squish)
|
||||
|
||||
State.IDLE:
|
||||
_current_squish = lerpf(_current_squish, 0.0, delta * 6.0)
|
||||
_breathe_time += delta * 2.2
|
||||
var breathe: float = sin(_breathe_time) * 0.025
|
||||
scale = Vector2(_base_scale.x * (1.0 + breathe), _base_scale.y * (1.0 - breathe))
|
||||
|
||||
State.SLEEP:
|
||||
_current_squish = lerpf(_current_squish, 0.0, delta * 6.0)
|
||||
_breathe_time += delta * 1.2
|
||||
var deep_breathe: float = sin(_breathe_time) * 0.018
|
||||
scale = Vector2(_base_scale.x * (1.0 - deep_breathe), _base_scale.y * (1.0 + deep_breathe))
|
||||
|
||||
func _update_debug_ui() -> void:
|
||||
if volume_bar:
|
||||
volume_bar.value = _current_rms
|
||||
if debug_label:
|
||||
var bpm_str = "Rilevamento..."
|
||||
if _current_bpm > 50.0:
|
||||
bpm_str = "%d BPM" % round(_current_bpm)
|
||||
|
||||
debug_label.text = "Stato: %s | Volume: %.2f | %s" % [
|
||||
State.keys()[current_state],
|
||||
_current_rms,
|
||||
bpm_str
|
||||
]
|
||||
@@ -0,0 +1 @@
|
||||
uid://b8bfvlbipx8j8
|
||||
@@ -0,0 +1,111 @@
|
||||
class_name SystemAudioAnalyzer
|
||||
extends Node
|
||||
|
||||
## Emette il valore RMS continuo [0.0 - 1.0] esclusivo dell'audio di sistema/applicazione
|
||||
signal volume_updated(rms_value: float)
|
||||
## Emette il BPM rilevato in tempo reale dalla musica
|
||||
signal bpm_updated(bpm: float)
|
||||
## Emette la lista delle applicazioni audio attive (es. Spotify, Chrome, Discord)
|
||||
signal applications_updated(apps: Array[Dictionary])
|
||||
|
||||
@export var udp_port: int = 4242
|
||||
@export var cmd_port: int = 4243
|
||||
|
||||
@export_group("Gain & Sensitivity")
|
||||
@export var system_audio_gain: float = 3.8
|
||||
@export var system_noise_floor: float = 0.0003
|
||||
|
||||
var _udp_peer: PacketPeerUDP
|
||||
var _udp_send: PacketPeerUDP
|
||||
var _loopback_pid: int = -1
|
||||
var _loopback_rms: float = 0.0
|
||||
var _last_bpm: float = 0.0
|
||||
var _watchdog_timer: float = 0.0
|
||||
|
||||
func _ready() -> void:
|
||||
_init_native_wasapi_loopback()
|
||||
|
||||
func _init_native_wasapi_loopback() -> void:
|
||||
_udp_peer = PacketPeerUDP.new()
|
||||
var err = _udp_peer.bind(udp_port, "*")
|
||||
if err != OK:
|
||||
err = _udp_peer.bind(udp_port, "127.0.0.1")
|
||||
|
||||
_udp_send = PacketPeerUDP.new()
|
||||
_udp_send.set_dest_address("127.0.0.1", cmd_port)
|
||||
|
||||
if err == OK:
|
||||
var exe_path = ProjectSettings.globalize_path("res://bin/LoopbackCapture.exe")
|
||||
if FileAccess.file_exists("res://bin/LoopbackCapture.exe"):
|
||||
_loopback_pid = OS.create_process(exe_path, [str(udp_port)], false)
|
||||
|
||||
## Invia al bridge la richiesta di ascoltare SOLO l'applicazione con il PID specificato (0 = Tutto)
|
||||
func select_application_pid(pid: int) -> void:
|
||||
if _udp_send:
|
||||
var msg = "PID:%d" % pid
|
||||
_udp_send.put_packet(msg.to_utf8_buffer())
|
||||
print("[SystemAudioAnalyzer] Applicazione audio selezionata PID: ", pid)
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
# Watchdog: se LoopbackCapture non è in esecuzione, riavvialo automaticamente
|
||||
_watchdog_timer += delta
|
||||
if _watchdog_timer >= 2.0:
|
||||
_watchdog_timer = 0.0
|
||||
if _loopback_pid <= 0 or not OS.is_process_running(_loopback_pid):
|
||||
var exe_path = ProjectSettings.globalize_path("res://bin/LoopbackCapture.exe")
|
||||
if FileAccess.file_exists("res://bin/LoopbackCapture.exe"):
|
||||
_loopback_pid = OS.create_process(exe_path, [str(udp_port)], false)
|
||||
|
||||
# Lettura esclusiva dell'Audio di Sistema o dell'Applicazione selezionata (Zero Microfono)
|
||||
if _udp_peer:
|
||||
var packet_received: bool = false
|
||||
while _udp_peer.get_available_packet_count() > 0:
|
||||
var packet: PackedByteArray = _udp_peer.get_packet()
|
||||
|
||||
# Pacchetto con la lista delle applicazioni attive (APPS:...)
|
||||
if packet.size() > 5 and packet[0] == 65 and packet[1] == 80 and packet[2] == 80 and packet[3] == 83 and packet[4] == 58:
|
||||
var text: String = packet.get_string_from_utf8()
|
||||
_parse_applications_list(text)
|
||||
continue
|
||||
|
||||
# Pacchetto audio standard: 8 byte (4 byte RMS + 4 byte BPM)
|
||||
if packet.size() >= 4:
|
||||
_loopback_rms = packet.decode_float(0)
|
||||
packet_received = true
|
||||
|
||||
if packet.size() >= 8:
|
||||
var incoming_bpm: float = packet.decode_float(4)
|
||||
if incoming_bpm > 50.0:
|
||||
_last_bpm = incoming_bpm
|
||||
bpm_updated.emit(incoming_bpm)
|
||||
|
||||
if not packet_received:
|
||||
_loopback_rms = move_toward(_loopback_rms, 0.0, delta * 3.0)
|
||||
|
||||
# Calcolo Volume Percepito (Curva Loudness)
|
||||
var processed_rms: float = 0.0
|
||||
if _loopback_rms > system_noise_floor:
|
||||
processed_rms = clampf(sqrt(_loopback_rms) * system_audio_gain, 0.0, 1.0)
|
||||
|
||||
volume_updated.emit(processed_rms)
|
||||
|
||||
func _parse_applications_list(raw_text: String) -> void:
|
||||
var payload = raw_text.substr(5)
|
||||
var tokens = payload.split(";")
|
||||
var apps: Array[Dictionary] = []
|
||||
for token in tokens:
|
||||
var parts = token.split(":")
|
||||
if parts.size() >= 2:
|
||||
var pid = int(parts[0])
|
||||
var name = parts[1]
|
||||
apps.append({"pid": pid, "name": name})
|
||||
if not apps.is_empty():
|
||||
applications_updated.emit(apps)
|
||||
|
||||
func _exit_tree() -> void:
|
||||
if _loopback_pid > 0:
|
||||
OS.kill(_loopback_pid)
|
||||
if _udp_peer:
|
||||
_udp_peer.close()
|
||||
if _udp_send:
|
||||
_udp_send.close()
|
||||
@@ -0,0 +1 @@
|
||||
uid://c76vsjv0asi61
|
||||
Reference in new issue
Block a user