diff --git a/tests/01-record-play-cs/Probe.csproj b/tests/01-record-play-cs/Probe.csproj index b92ac93..94d3d81 100644 --- a/tests/01-record-play-cs/Probe.csproj +++ b/tests/01-record-play-cs/Probe.csproj @@ -10,6 +10,7 @@ true false true + true diff --git a/tests/01-record-play-cs/Program.cs b/tests/01-record-play-cs/Program.cs index b810bd1..ef50c72 100644 --- a/tests/01-record-play-cs/Program.cs +++ b/tests/01-record-play-cs/Program.cs @@ -1,22 +1,130 @@ +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 count = PortAudio.DeviceCount; - Console.WriteLine($"PortAudio version: {PortAudio.VersionInfo.versionText}"); - Console.WriteLine($"Devices ({count}):"); - for (int i = 0; i < count; i++) + 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 { - var info = PortAudio.GetDeviceInfo(i); - Console.WriteLine( - $" [{i}] {info.name} in={info.maxInputChannels} out={info.maxOutputChannels} " + - $"defaultSr={info.defaultSampleRate}"); + 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); +}