using System.Runtime.InteropServices; using PortAudioSharp; using RecordPlayProbe; using Stream = PortAudioSharp.Stream; const int SampleRate = 16_000; const int Channels = 1; const int DurationS = 5; const uint BlockFrames = 1024; const string OutWav = "out.wav"; // Tell PortAudio's ALSA host API to wrap devices in `plughw:` so it does // rate/format conversion (USB Speaker Phone does not natively offer 16 kHz). // Environment.SetEnvironmentVariable updates the .NET table but the libc // getenv() view PortAudio actually reads can lag, so call setenv directly too. Environment.SetEnvironmentVariable("PA_ALSA_PLUGHW", "1"); Libc.setenv("PA_ALSA_PLUGHW", "1", 1); PortAudio.Initialize(); try { int device = FindUsbDevice(); Console.WriteLine( $"Recording {DurationS}s from device {device} " + $"('{PortAudio.GetDeviceInfo(device).name}')"); short[] buf = new short[SampleRate * DurationS]; int writeOffset = 0; object gate = new(); var inParams = new StreamParameters { device = device, channelCount = Channels, sampleFormat = SampleFormat.Int16, suggestedLatency = PortAudio.GetDeviceInfo(device).defaultLowInputLatency, hostApiSpecificStreamInfo = IntPtr.Zero, }; Stream.Callback callback = (IntPtr input, IntPtr _, uint frameCount, ref StreamCallbackTimeInfo _2, StreamCallbackFlags status, IntPtr _3) => { if (status.HasFlag(StreamCallbackFlags.InputOverflow)) Console.Error.WriteLine("[status] input overflow"); lock (gate) { int remaining = buf.Length - writeOffset; int take = (int)Math.Min(frameCount, (uint)remaining); if (take > 0) { unsafe { short* src = (short*)input.ToPointer(); fixed (short* dst = &buf[writeOffset]) { Buffer.MemoryCopy(src, dst, take * sizeof(short), take * sizeof(short)); } } writeOffset += take; } } return StreamCallbackResult.Continue; }; using var stream = new Stream( inParams, null, SampleRate, BlockFrames, StreamFlags.NoFlag, callback, IntPtr.Zero); stream.Start(); var start = DateTime.UtcNow; while (true) { int written; lock (gate) { written = writeOffset; } if (written >= buf.Length) break; double level = Rms(buf, Math.Max(0, written - 1600), written); int bars = (int)(level * 60); double elapsed = (DateTime.UtcNow - start).TotalSeconds; Console.Write($"\r{elapsed,4:0.0}s |{new string('#', bars).PadRight(60)}|"); Thread.Sleep(50); } Console.WriteLine(); stream.Stop(); WavWriter.Write(OutWav, buf, SampleRate); Console.WriteLine($"Wrote {OutWav} ({buf.Length} frames @ {SampleRate} Hz)"); } finally { PortAudio.Terminate(); } static int FindUsbDevice() { int count = PortAudio.DeviceCount; for (int i = 0; i < count; i++) { var info = PortAudio.GetDeviceInfo(i); if (info.name.ToLowerInvariant().Contains("usb") && info.maxInputChannels >= 1) return i; } Console.Error.WriteLine("USB audio device not found. Devices:"); for (int i = 0; i < count; i++) { var info = PortAudio.GetDeviceInfo(i); Console.Error.WriteLine( $" [{i}] {info.name} in={info.maxInputChannels} out={info.maxOutputChannels}"); } Environment.Exit(1); return -1; // unreachable } static double Rms(short[] data, int from, int to) { if (to <= from) return 0.0; double sumSq = 0.0; for (int i = from; i < to; i++) { double x = data[i] / 32768.0; sumSq += x * x; } return Math.Sqrt(sumSq / (to - from) + 1e-12); } static class Libc { [DllImport("libc", EntryPoint = "setenv")] public static extern int setenv(string name, string value, int overwrite); }