using System; using System.IO; using System.Runtime.InteropServices; using System.Threading; namespace FrontTerrain.Scout { public class ScoutAudioRecorder { [StructLayout(LayoutKind.Sequential)] public struct WAVEFORMATEX { public ushort wFormatTag; public ushort nChannels; public uint nSamplesPerSec; public uint nAvgBytesPerSec; public ushort nBlockAlign; public ushort wBitsPerSample; public ushort cbSize; } [StructLayout(LayoutKind.Sequential)] public struct WAVEHDR { public IntPtr lpData; public uint dwBufferLength; public uint dwBytesRecorded; public IntPtr dwUser; public uint dwFlags; public uint dwLoops; public IntPtr lpNext; public IntPtr reserved; } [DllImport("winmm.dll")] public static extern int waveInOpen(out IntPtr phwi, int uDeviceID, ref WAVEFORMATEX lpFormat, IntPtr dwCallback, IntPtr dwInstance, uint fdwOpen); [DllImport("winmm.dll")] public static extern int waveInPrepareHeader(IntPtr hwi, ref WAVEHDR pwh, int cbwh); [DllImport("winmm.dll")] public static extern int waveInAddBuffer(IntPtr hwi, ref WAVEHDR pwh, int cbwh); [DllImport("winmm.dll")] public static extern int waveInStart(IntPtr hwi); [DllImport("winmm.dll")] public static extern int waveInStop(IntPtr hwi); [DllImport("winmm.dll")] public static extern int waveInReset(IntPtr hwi); [DllImport("winmm.dll")] public static extern int waveInClose(IntPtr hwi); [DllImport("winmm.dll")] public static extern int waveInUnprepareHeader(IntPtr hwi, ref WAVEHDR pwh, int cbwh); public static int Main(string[] args) { if (args.Length == 0) { Console.WriteLine("USAGE: scout_audio_recorder.exe [maxSeconds]"); return 1; } string outFile = args[0]; int maxSec = 18; int parsedSec; if (args.Length > 1 && int.TryParse(args[1], out parsedSec) && parsedSec > 0) { maxSec = Math.Min(60, parsedSec); } int silenceCutoffMs = 2500; // 2.5 seconds silence debounce int parsedSilence; if (args.Length > 2 && int.TryParse(args[2], out parsedSilence) && parsedSilence >= 500) { silenceCutoffMs = parsedSilence; } WAVEFORMATEX format = new WAVEFORMATEX(); format.wFormatTag = 1; // PCM format.nChannels = 1; // Mono format.nSamplesPerSec = 16000; // 16kHz format.wBitsPerSample = 16; format.nBlockAlign = (ushort)(format.nChannels * (format.wBitsPerSample / 8)); format.nAvgBytesPerSec = format.nSamplesPerSec * format.nBlockAlign; format.cbSize = 0; IntPtr hWaveIn; int res = waveInOpen(out hWaveIn, -1, ref format, IntPtr.Zero, IntPtr.Zero, 0); if (res != 0) { Console.WriteLine("MIC_OPEN_FAILED:" + res); return 2; } uint totalBufferSize = (uint)(format.nAvgBytesPerSec * maxSec); IntPtr pBuffer = Marshal.AllocHGlobal((int)totalBufferSize); WAVEHDR header = new WAVEHDR(); header.lpData = pBuffer; header.dwBufferLength = totalBufferSize; header.dwBytesRecorded = 0; header.dwFlags = 0; waveInPrepareHeader(hWaveIn, ref header, Marshal.SizeOf(header)); waveInAddBuffer(hWaveIn, ref header, Marshal.SizeOf(header)); waveInStart(hWaveIn); Console.WriteLine("RECORDING_ACTIVE"); int checkIntervalMs = 100; int elapsedMs = 0; bool speechDetected = false; int silenceAfterSpeechMs = 0; const int SPEECH_RMS_THRESHOLD = 450; try { while (elapsedMs < maxSec * 1000) { Thread.Sleep(checkIntervalMs); elapsedMs += checkIntervalMs; uint recorded = header.dwBytesRecorded; if (recorded >= 3200) // at least 100ms { int samplesToAnalyze = 1600; int offset = (int)recorded - (samplesToAnalyze * 2); if (offset < 0) offset = 0; long sumSquares = 0; int sampleCount = 0; for (int i = offset; i < recorded - 1; i += 2) { short sample = Marshal.ReadInt16(pBuffer, i); sumSquares += (long)sample * sample; sampleCount++; } double rms = sampleCount > 0 ? Math.Sqrt((double)sumSquares / sampleCount) : 0; if (rms >= SPEECH_RMS_THRESHOLD) { if (!speechDetected) { speechDetected = true; Console.WriteLine("SPEECH_DETECTED"); } silenceAfterSpeechMs = 0; } else if (speechDetected) { silenceAfterSpeechMs += checkIntervalMs; if (silenceAfterSpeechMs >= silenceCutoffMs) { // Natural speech ended and silence debounce followed -> finish recording break; } } } } } catch { } waveInStop(hWaveIn); waveInReset(hWaveIn); waveInUnprepareHeader(hWaveIn, ref header, Marshal.SizeOf(header)); waveInClose(hWaveIn); uint finalBytes = header.dwBytesRecorded; if (finalBytes > 0) { byte[] pcmData = new byte[finalBytes]; Marshal.Copy(pBuffer, pcmData, 0, (int)finalBytes); string dir = Path.GetDirectoryName(outFile); if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir)) { Directory.CreateDirectory(dir); } using (FileStream fs = new FileStream(outFile, FileMode.Create, FileAccess.Write)) using (BinaryWriter bw = new BinaryWriter(fs)) { bw.Write(System.Text.Encoding.ASCII.GetBytes("RIFF")); bw.Write((uint)(36 + finalBytes)); bw.Write(System.Text.Encoding.ASCII.GetBytes("WAVE")); bw.Write(System.Text.Encoding.ASCII.GetBytes("fmt ")); bw.Write((uint)16); bw.Write(format.wFormatTag); bw.Write(format.nChannels); bw.Write(format.nSamplesPerSec); bw.Write(format.nAvgBytesPerSec); bw.Write(format.nBlockAlign); bw.Write(format.wBitsPerSample); bw.Write(System.Text.Encoding.ASCII.GetBytes("data")); bw.Write(finalBytes); bw.Write(pcmData); } Console.WriteLine("SAVED:" + finalBytes); } else { Console.WriteLine("NO_AUDIO"); } Marshal.FreeHGlobal(pBuffer); return 0; } } }