187 lines
5.7 KiB
C#
187 lines
5.7 KiB
C#
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)");
|
|
|
|
Console.WriteLine($"Playing back through device {device}");
|
|
|
|
int playOffset = 0;
|
|
object playGate = new();
|
|
var doneEvent = new ManualResetEventSlim(false);
|
|
|
|
var outParams = new StreamParameters
|
|
{
|
|
device = device,
|
|
channelCount = Channels,
|
|
sampleFormat = SampleFormat.Int16,
|
|
suggestedLatency = PortAudio.GetDeviceInfo(device).defaultLowOutputLatency,
|
|
hostApiSpecificStreamInfo = IntPtr.Zero,
|
|
};
|
|
|
|
Stream.Callback playCb = (IntPtr _, IntPtr output, uint frameCount,
|
|
ref StreamCallbackTimeInfo _2, StreamCallbackFlags _3, IntPtr _4) =>
|
|
{
|
|
lock (playGate)
|
|
{
|
|
int remaining = buf.Length - playOffset;
|
|
int take = (int)Math.Min(frameCount, (uint)remaining);
|
|
int silence = (int)frameCount - take;
|
|
|
|
unsafe
|
|
{
|
|
short* dst = (short*)output.ToPointer();
|
|
if (take > 0)
|
|
{
|
|
fixed (short* src = &buf[playOffset])
|
|
Buffer.MemoryCopy(src, dst, take * sizeof(short), take * sizeof(short));
|
|
playOffset += take;
|
|
}
|
|
if (silence > 0)
|
|
{
|
|
for (int i = take; i < frameCount; i++) dst[i] = 0;
|
|
}
|
|
}
|
|
|
|
if (playOffset >= buf.Length)
|
|
{
|
|
doneEvent.Set();
|
|
return StreamCallbackResult.Complete;
|
|
}
|
|
}
|
|
return StreamCallbackResult.Continue;
|
|
};
|
|
|
|
using var playStream = new Stream(
|
|
null, outParams, SampleRate, BlockFrames, StreamFlags.NoFlag, playCb, IntPtr.Zero);
|
|
playStream.Start();
|
|
doneEvent.Wait();
|
|
// Give PortAudio a moment to drain the device buffer before Stop().
|
|
Thread.Sleep(200);
|
|
playStream.Stop();
|
|
}
|
|
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);
|
|
}
|