namespace Nonemm.Recorder.Tests; public class RecorderEngineTests : IDisposable { private readonly string path = Path.Combine(Path.GetTempPath(), $"recorder-{Guid.NewGuid():N}.opus"); private readonly FakeBackend backend = new(); private readonly RecorderEngine engine; public RecorderEngineTests() { engine = new RecorderEngine(backend); } public void Dispose() { engine.Dispose(); File.Delete(path); } [Fact] public void A_channel_of_a_missing_device_stays_in_the_list_as_unavailable() { RecorderChannel channel = engine.AddChannel(new ChannelSource("Gone", false, 0, 1)); Assert.False(channel.IsAvailable); Assert.Single(engine.Channels); Assert.Equal("not connected", channel.Problem); } [Fact] public void A_device_that_appears_is_opened_and_its_channel_becomes_available() { RecorderChannel channel = engine.AddChannel(new ChannelSource("Mic", false, 0, 1)); backend.Devices.Add(new AudioDeviceInfo("Mic", false, 1)); engine.Refresh(); Assert.True(channel.IsAvailable); Assert.True(backend.IsOpen("Mic")); } [Fact] public void A_device_that_is_unplugged_is_closed_and_its_channel_is_kept() { backend.Devices.Add(new AudioDeviceInfo("Mic", false, 1)); RecorderChannel channel = engine.AddChannel(new ChannelSource("Mic", false, 0, 1)); backend.Devices.Clear(); engine.Refresh(); Assert.False(channel.IsAvailable); Assert.False(backend.IsOpen("Mic")); Assert.Single(engine.Channels); } [Fact] public void A_channel_past_the_end_of_the_device_is_unavailable() { backend.Devices.Add(new AudioDeviceInfo("Line In", false, 2)); RecorderChannel channel = engine.AddChannel(new ChannelSource("Line In", false, 5, 8)); Assert.False(channel.IsAvailable); } [Fact] public void The_two_sides_of_a_stereo_device_are_two_channels() { backend.Devices.Add(new AudioDeviceInfo("Line In", false, 2)); RecorderChannel left = engine.AddChannel(new ChannelSource("Line In", false, 0, 2)); RecorderChannel right = engine.AddChannel(new ChannelSource("Line In", false, 1, 2)); backend.Push("Line In", 2, 480, channel: 1, value: 0.5f); Assert.Equal(0f, left.TakePeak()); Assert.Equal(0.5f, right.TakePeak()); } [Fact] public void An_unavailable_channel_is_recorded_as_silence_and_picks_up_when_it_appears() { backend.Devices.Add(new AudioDeviceInfo("Mic", false, 1)); engine.AddChannel(new ChannelSource("Mic", false, 0, 1)); engine.AddChannel(new ChannelSource("Radio", false, 0, 1)); engine.StartRecording(path, 32); Feed(seconds: 1, "Mic"); backend.Devices.Add(new AudioDeviceInfo("Radio", false, 1)); engine.Refresh(); Feed(seconds: 1, "Mic", "Radio"); engine.StopRecording(); OggOpusReader file = OggOpusReader.Read(path); Assert.Equal(2, file.Channels); int second = RecorderEngine.SampleRate; Assert.True(OggOpusReader.Rms(file.Pcm[0], second / 4, second / 2) > 0.2f); Assert.True(OggOpusReader.Rms(file.Pcm[1], second / 4, second / 2) < 0.01f); Assert.True(OggOpusReader.Rms(file.Pcm[1], second + second / 4, second / 2) > 0.2f); } [Fact] public void The_channels_cannot_change_during_a_recording() { backend.Devices.Add(new AudioDeviceInfo("Mic", false, 1)); engine.AddChannel(new ChannelSource("Mic", false, 0, 1)); engine.StartRecording(path, 32); Assert.Throws(() => engine.AddChannel(new ChannelSource("Mic", false, 0, 1))); } [Fact] public void Monitoring_opens_the_playback_device_once_and_closes_it_with_the_last_channel() { backend.Devices.Add(new AudioDeviceInfo("Mic", false, 1)); RecorderChannel first = engine.AddChannel(new ChannelSource("Mic", false, 0, 1)); RecorderChannel second = engine.AddChannel(new ChannelSource("Mic", false, 0, 1)); engine.SetMonitored(first, true); engine.SetMonitored(second, true); engine.SetMonitored(first, false); Assert.Equal(1, backend.PlaybackOpened); engine.SetMonitored(second, false); engine.SetMonitored(first, true); Assert.Equal(2, backend.PlaybackOpened); } // a tone in real time, 10 ms at a time, as a device delivers it private void Feed(int seconds, params string[] names) { for (int step = 0; step < seconds * 100; step++) { foreach (string name in names) { backend.Push(name, 1, 480, channel: 0, value: step % 2 == 0 ? 0.4f : -0.4f); } Thread.Sleep(10); } } }