Add a sound recorder that writes several devices to one Opus file
nonemm-recorder is a separate program (src/Nonemm.Recorder.App) over a library (src/Nonemm.Recorder). Each row in its list is one channel of one device, and each row becomes one channel in an Ogg Opus file: 60 ms frames, complexity 9, 32 kbps per channel by default. - A device that is not connected keeps its row, in red, and writes silence. It is recorded from the moment it returns, also during a recording. - Output devices are recorded as loopback: WASAPI on Windows, the "Monitor of" sources on PulseAudio and PipeWire. - A headphone button plays one channel, in mono, on the default device. - The rows, bitrate and folder are saved when the window closes. SoundFlow 1.4.2 numbers its backend enum one higher than the miniaudio it ships, so asking for PulseAudio gave ALSA and Wasapi gave DirectSound. SoundFlowBackend lowers the value by one and checks which backend started. Output devices and live capture are not tested on real hardware. The tests use a fake sound system and decode the written file. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
namespace Nonemm.Recorder;
|
||||
|
||||
/// A fixed-size queue of samples between the audio thread that fills it and
|
||||
/// the thread that drains it. When it is full, the oldest samples are dropped.
|
||||
public sealed class SampleRing
|
||||
{
|
||||
private readonly float[] buffer;
|
||||
private readonly object gate = new();
|
||||
private int start;
|
||||
private int count;
|
||||
|
||||
public SampleRing(int capacity)
|
||||
{
|
||||
buffer = new float[capacity];
|
||||
}
|
||||
|
||||
public int Count
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (gate)
|
||||
{
|
||||
return count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Write(ReadOnlySpan<float> samples)
|
||||
{
|
||||
lock (gate)
|
||||
{
|
||||
if (samples.Length > buffer.Length)
|
||||
{
|
||||
samples = samples[^buffer.Length..];
|
||||
}
|
||||
int overflow = count + samples.Length - buffer.Length;
|
||||
if (overflow > 0)
|
||||
{
|
||||
start = (start + overflow) % buffer.Length;
|
||||
count -= overflow;
|
||||
}
|
||||
int end = (start + count) % buffer.Length;
|
||||
int first = Math.Min(samples.Length, buffer.Length - end);
|
||||
samples[..first].CopyTo(buffer.AsSpan(end));
|
||||
samples[first..].CopyTo(buffer);
|
||||
count += samples.Length;
|
||||
}
|
||||
}
|
||||
|
||||
/// Fills `destination` with the oldest samples and returns how many it
|
||||
/// got. The rest of `destination` is not touched.
|
||||
public int Read(Span<float> destination)
|
||||
{
|
||||
lock (gate)
|
||||
{
|
||||
int taken = Math.Min(destination.Length, count);
|
||||
int first = Math.Min(taken, buffer.Length - start);
|
||||
buffer.AsSpan(start, first).CopyTo(destination);
|
||||
buffer.AsSpan(0, taken - first).CopyTo(destination[first..]);
|
||||
start = (start + taken) % buffer.Length;
|
||||
count -= taken;
|
||||
return taken;
|
||||
}
|
||||
}
|
||||
|
||||
/// Drops the oldest samples until at most `keep` are left.
|
||||
public void TrimTo(int keep)
|
||||
{
|
||||
lock (gate)
|
||||
{
|
||||
int drop = count - keep;
|
||||
if (drop > 0)
|
||||
{
|
||||
start = (start + drop) % buffer.Length;
|
||||
count = keep;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
lock (gate)
|
||||
{
|
||||
start = 0;
|
||||
count = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user