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>
56 lines
1.8 KiB
C#
56 lines
1.8 KiB
C#
namespace Nonemm.Recorder;
|
|
|
|
/// A capture device in use: takes its interleaved samples apart and hands each
|
|
/// one to the channel that records it.
|
|
internal sealed class OpenDevice
|
|
{
|
|
private readonly float[] scratch = new float[RecorderEngine.SampleRate];
|
|
private volatile RecorderChannel?[] byDeviceChannel;
|
|
|
|
public OpenDevice(AudioDeviceInfo info)
|
|
{
|
|
Info = info;
|
|
byDeviceChannel = new RecorderChannel?[Math.Max(1, info.Channels)];
|
|
}
|
|
|
|
public AudioDeviceInfo Info { get; }
|
|
|
|
public IDisposable Handle { get; set; } = null!; // set right after the device opens
|
|
|
|
/// Sets which channel gets which of the device's channels.
|
|
public void Route(IEnumerable<RecorderChannel> users)
|
|
{
|
|
RecorderChannel?[] routing = new RecorderChannel?[byDeviceChannel.Length];
|
|
foreach (RecorderChannel user in users)
|
|
{
|
|
if (user.Source.Channel < routing.Length)
|
|
{
|
|
routing[user.Source.Channel] = user;
|
|
}
|
|
}
|
|
byDeviceChannel = routing;
|
|
}
|
|
|
|
public void OnSamples(ReadOnlySpan<float> interleaved)
|
|
{
|
|
RecorderChannel?[] routing = byDeviceChannel;
|
|
int width = routing.Length;
|
|
int frames = Math.Min(interleaved.Length / width, scratch.Length);
|
|
for (int index = 0; index < width; index++)
|
|
{
|
|
if (routing[index] is not { } channel)
|
|
{
|
|
continue;
|
|
}
|
|
float level = 0;
|
|
for (int frame = 0; frame < frames; frame++)
|
|
{
|
|
float sample = interleaved[frame * width + index];
|
|
scratch[frame] = sample;
|
|
level = Math.Max(level, Math.Abs(sample));
|
|
}
|
|
channel.Add(scratch.AsSpan(0, frames), level);
|
|
}
|
|
}
|
|
}
|