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