Record loop: callback InputStream + RMS meter + WAV write

This commit is contained in:
2026-06-12 09:54:43 +00:00
parent 1d08f49204
commit e2bd845abe
2 changed files with 117 additions and 8 deletions
+1
View File
@@ -10,6 +10,7 @@
<SelfContained>true</SelfContained>
<PublishSingleFile>false</PublishSingleFile>
<InvariantGlobalization>true</InvariantGlobalization>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="PortAudioSharp2" Version="1.0.6" />
+116 -8
View File
@@ -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++)
{
var info = PortAudio.GetDeviceInfo(i);
int device = FindUsbDevice();
Console.WriteLine(
$" [{i}] {info.name} in={info.maxInputChannels} out={info.maxOutputChannels} " +
$"defaultSr={info.defaultSampleRate}");
$"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);
}