using VoiceCat.Windows; using VoiceCat.Server.Data; using Voicecat.V1; using static VoiceCat.Tests.ServerTests; using Client = VoiceCat.Windows.VoiceCatClient; namespace VoiceCat.Tests; public class WindowsManagedClientTests { private static readonly AudioConfigInfo DefaultAudio = new(0, false, 48_000, 64_000, 20, 0, true, 10, false, 10, false); [Fact] public void ChannelHierarchyUsesServerSortOrderAndKeepsIdsWithDisplayedChannels() { ChannelInfo[] channels = [ new(10, 0, "Zulu", "", false, 0, -10, DefaultAudio), new(20, 0, "Alpha", "", false, 0, 20, DefaultAudio), new(11, 10, "Zulu child", "", false, 0, 10, DefaultAudio), new(12, 10, "Alpha child", "", false, 0, 0, DefaultAudio), new(30, 0, "Beta", "", false, 0, 20, DefaultAudio), ]; IReadOnlyList roots = ChannelHierarchy.Build(channels); Assert.Equal([(10U, "Zulu"), (20U, "Alpha"), (30U, "Beta")], roots.Select(item => (item.Channel.Id, item.Channel.Name))); Assert.Equal([(12U, "Alpha child"), (11U, "Zulu child")], roots[0].Children.Select(item => (item.Channel.Id, item.Channel.Name))); } private static async Task Until(Client client, Func predicate) { using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10)); do { client.PumpEvents(); if (predicate()) return; await Task.Delay(10, timeout.Token); } while (true); } private static async Task Login(Client client, ServerFixture fixture, bool admin = false) { client.EventReceived += e => { if (e.Type == VcEventType.ServerIdentity) client.ConfirmServerIdentity(true); }; Assert.Equal(VcResult.Ok, client.Connect("127.0.0.1", (ushort)fixture.Server.EndPoint.Port)); if (admin) client.AuthenticateUser("Admin", "secret"); else client.AuthenticateGuest("Guest"); await Until(client, () => client.ListUsers().Count > 0); } [Fact] public async Task ShippedWindowsFacadeChatsExchangesPcmAndKeepsCaptureIdsAcrossChannelMoves() { await using var fixture = new ServerFixture(); using (var accounts = new AccountStore(Path.Combine(fixture.Directory, "voicecat.db"))) await accounts.CreateAccountAsync("Admin", "secret", true); using var alice = new Client("Alice", "test", tofuStorePath: Path.Combine(fixture.Directory, "alice.pins")); using var bob = new Client("Bob", "test", tofuStorePath: Path.Combine(fixture.Directory, "bob.pins")); await Login(alice, fixture, true); await Login(bob, fixture); Assert.True(alice.GetPermissions().IsAdmin); string? body = null; bob.EventReceived += e => { if (e.Type == VcEventType.TextMessage) body = e.Text; }; Assert.Equal(VcResult.Ok, alice.SendText(VcTextScope.Channel, 1, "Managed Windows chat")); await Until(bob, () => body is not null); Assert.Equal("Managed Windows chat", body); Assert.Equal(VcResult.Ok, alice.JoinVoice()); Assert.Equal(VcResult.Ok, bob.JoinVoice()); alice.SetInputMode(VcInputMode.AlwaysOn); bob.SetInputMode(VcInputMode.AlwaysOn); var a = alice.StartStreamExternalFeed(VcStreamKind.Mic, "Mic"); var b = bob.StartStreamExternalFeed(VcStreamKind.Mic, "Mic"); Assert.Equal(VcResult.Ok, a.Result); Assert.Equal(VcResult.Ok, b.Result); await Until(bob, () => bob.ManagedClient.Users.Any(u => u.Streams.Count > 0 && u.Id != bob.ManagedClient.Authentication!.Self.Id)); long aliceEnergy = 0, bobEnergy = 0; alice.ManagedClient.Audio.MixedPcm += pcm => { long sum = 0; foreach (short sample in pcm) sum += Math.Abs((int)sample); Interlocked.Add(ref aliceEnergy, sum); }; bob.ManagedClient.Audio.MixedPcm += pcm => { long sum = 0; foreach (short sample in pcm) sum += Math.Abs((int)sample); Interlocked.Add(ref bobEnergy, sum); }; short[] tone = Enumerable.Range(0, 960).Select(i => (short)(8000 * Math.Sin(i * Math.PI * 880 / 48000))).ToArray(); for (int i = 0; i < 40; i++) { alice.StreamFeedPcm(a.StreamId, tone, 960, 1); bob.StreamFeedPcm(b.StreamId, tone, 960, 1); alice.PumpEvents(); bob.PumpEvents(); await Task.Delay(20); } Assert.True(Interlocked.Read(ref aliceEnergy) > 100000); Assert.True(Interlocked.Read(ref bobEnergy) > 100000); uint oldId = alice.ManagedClient.LocalStreams.Single().StreamId; await alice.ManagedClient.RequestAsync(new() { CreateChannel = new() { Channel = new() { Name = "Stereo", ParentId = 1, Audio = AudioEngineTests.Stream(20, true).Audio } } }); await Until(alice, () => alice.ListChannels().Any(c => c.Name == "Stereo")); uint channel = alice.ListChannels().Single(c => c.Name == "Stereo").Id; Assert.Equal(VcResult.Ok, alice.JoinChannel(channel)); await Until(alice, () => alice.ManagedClient.LocalStreams.Any(s => s.StreamId != oldId)); Assert.Equal(a.StreamId, Assert.Single(alice.ListUserStreams(alice.ManagedClient.Authentication!.Self.Id)).StreamId); Assert.True(alice.GetStreamAudioConfig(alice.ManagedClient.Authentication!.Self.Id, a.StreamId).Config!.Stereo); Assert.Equal(VcResult.Ok, alice.StreamFeedPcm(a.StreamId, tone, 960, 1)); Assert.Equal(VcResult.Ok, alice.StopStream(a.StreamId)); Assert.Empty(alice.ManagedClient.LocalStreams); } [Fact] public async Task WindowsChannelEditorSettingsRoundTripFromServer() { await using var fixture = new ServerFixture(); using (var accounts = new AccountStore(Path.Combine(fixture.Directory, "voicecat.db"))) await accounts.CreateAccountAsync("Admin", "secret", true); using var client = new Client("Admin", "test", tofuStorePath: Path.Combine(fixture.Directory, "admin.pins")); await Login(client, fixture, true); ChannelInfo music = client.ListChannels().Single(c => c.Name == "Music Room"); var expected = new AudioConfigInfo(0, true, 48_000, 128_000, 20, 1, true, 15, true, 10, true); var edit = new ChannelEditInfo(music.Id, music.ParentId, music.Name, music.Topic, music.PasswordProtected, null, music.MaxUsers, music.SortOrder, expected); Assert.Equal(VcResult.Ok, client.EditChannel(edit)); await Until(client, () => client.ListChannels().Single(c => c.Id == music.Id).Audio.Dred); Assert.Equal(expected, client.ListChannels().Single(c => c.Id == music.Id).Audio); } [Fact] public async Task WindowsScreenAudioPreservesDistinctStereoChannels() { await using var fixture = new ServerFixture(); using (var accounts = new AccountStore(Path.Combine(fixture.Directory, "voicecat.db"))) await accounts.CreateAccountAsync("Admin", "secret", true); using var alice = new Client("Alice", "test", tofuStorePath: Path.Combine(fixture.Directory, "alice-screen.pins")); using var bob = new Client("Bob", "test", tofuStorePath: Path.Combine(fixture.Directory, "bob-screen.pins")); await Login(alice, fixture, true); await Login(bob, fixture); await alice.ManagedClient.RequestAsync(new() { CreateChannel = new() { Channel = new() { Name = "Stereo screen", ParentId = 1, Audio = AudioEngineTests.Stream(20, true).Audio } } }); await Until(alice, () => alice.ListChannels().Any(c => c.Name == "Stereo screen")); uint channel = alice.ListChannels().Single(c => c.Name == "Stereo screen").Id; Assert.Equal(VcResult.Ok, alice.JoinChannel(channel)); Assert.Equal(VcResult.Ok, bob.JoinChannel(channel)); await Until(alice, () => alice.ListUsers().Count(u => u.ChannelId == channel) == 2); Assert.Equal(VcResult.Ok, alice.JoinVoice()); Assert.Equal(VcResult.Ok, bob.JoinVoice()); var screen = alice.StartStreamExternalFeed(VcStreamKind.ScreenAudio, "Screen audio"); Assert.Equal(VcResult.Ok, screen.Result); await Until(bob, () => bob.ManagedClient.Users.Any(u => u.Id != bob.ManagedClient.Authentication!.Self.Id && u.Streams.Any(s => s.Kind == StreamKind.StreamScreenAudio))); long separation = 0; int receivedChannels = 0; bob.ManagedClient.Audio.StreamPcm += (_, _, pcm, channels) => { receivedChannels = channels; if (channels == 2) for (int i = 0; i < pcm.Length; i += 2) Interlocked.Add(ref separation, Math.Abs((int)pcm[i] - pcm[i + 1])); }; short[] stereo = new short[1920]; for (int i = 0; i < 960; i++) { stereo[2 * i] = (short)(8000 * Math.Sin(i * Math.PI * 880 / 48000)); stereo[2 * i + 1] = (short)(8000 * Math.Sin(i * Math.PI * 1320 / 48000)); } for (int i = 0; i < 40; i++) { Assert.Equal(VcResult.Ok, alice.StreamFeedPcm(screen.StreamId, stereo, 960, 2)); alice.PumpEvents(); bob.PumpEvents(); await Task.Delay(20); } Assert.Equal(2, receivedChannels); Assert.True(Interlocked.Read(ref separation) > 100_000); } }