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); } }