Files
Nonemm/tests/Nonemm.Recorder.Tests/OpusFileWriterTests.cs
T
erikbrockoandClaude Sonnet 5.5 c01032a2cc 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>
2026-10-23 23:47:45 +00:00

66 lines
2.3 KiB
C#

namespace Nonemm.Recorder.Tests;
public class OpusFileWriterTests : IDisposable
{
private readonly string path = Path.Combine(Path.GetTempPath(), $"recorder-{Guid.NewGuid():N}.opus");
public void Dispose() => File.Delete(path);
private void Write(int channels, int frames, Func<int, int, float> sample)
{
using OpusFileWriter writer = new(File.Create(path), channels, 32);
for (int frame = 0; frame < frames; frame++)
{
float[] interleaved = new float[OpusFileWriter.FrameSamples * channels];
for (int i = 0; i < OpusFileWriter.FrameSamples; i++)
{
for (int channel = 0; channel < channels; channel++)
{
interleaved[i * channels + channel] = sample(channel, frame * OpusFileWriter.FrameSamples + i);
}
}
writer.WriteFrame(interleaved);
}
}
private static float Tone(int hertz, int index) =>
0.5f * (float)Math.Sin(2 * Math.PI * hertz * index / RecorderEngine.SampleRate);
[Fact]
public void Each_channel_comes_back_on_its_own_channel()
{
Write(3, 10, (channel, index) => channel == 1 ? 0 : Tone(channel == 0 ? 440 : 1000, index));
OggOpusReader file = OggOpusReader.Read(path);
Assert.Equal(3, file.Channels);
Assert.Equal(255, file.MappingFamily);
Assert.True(OggOpusReader.Rms(file.Pcm[0], 4800, 9600) > 0.25f);
Assert.True(OggOpusReader.Rms(file.Pcm[1], 4800, 9600) < 0.01f);
Assert.True(OggOpusReader.Rms(file.Pcm[2], 4800, 9600) > 0.25f);
}
[Fact]
public void The_end_granule_is_the_length_of_the_audio_plus_the_pre_skip()
{
Write(2, 10, (_, index) => Tone(440, index));
OggOpusReader file = OggOpusReader.Read(path);
Assert.True(file.HasEndMark);
Assert.Equal(10L * OpusFileWriter.FrameSamples + file.PreSkip, file.FinalGranule);
}
[Fact]
public void One_channel_is_written_as_a_plain_mono_file()
{
Write(1, 5, (_, index) => Tone(440, index));
OggOpusReader file = OggOpusReader.Read(path);
Assert.Equal(1, file.Channels);
Assert.Equal(0, file.MappingFamily);
Assert.True(OggOpusReader.Rms(file.Pcm[0], 4800, 9600) > 0.25f);
}
}