using System.IO.MemoryMappedFiles; using VoiceCat.Core; using Voicecat.V1; namespace VoiceCat.iOS; internal sealed class BroadcastAudioPump : IAsyncDisposable { private const uint Magic = 0x56434252, Version = 1; private const int Header = 64, Capacity = 96_000, Frame = 960; private readonly CancellationTokenSource stop = new(); private Task worker = Task.CompletedTask; private VoiceCatClient? client; private uint streamId; private readonly short[] scratch = new short[Frame * 2]; internal void Start(VoiceCatClient owner) { client = owner; worker = RunAsync(stop.Token); } private async Task RunAsync(CancellationToken token) { while (!token.IsCancellationRequested) { try { await DrainAsync(token); } catch (Exception exception) when (exception is IOException or UnauthorizedAccessException or InvalidDataException) { } await Task.Delay(10, token).ConfigureAwait(false); } } private async Task DrainAsync(CancellationToken token) { NSUrl? root = NSFileManager.DefaultManager.GetContainerUrl(IosConstants.AppGroup); if (root?.Path is null) return; string path = Path.Combine(root.Path, "voicecat", "broadcast_audio.ring"); if (!File.Exists(path)) return; using MemoryMappedFile map = MemoryMappedFile.CreateFromFile(path, FileMode.Open, null, Header + Capacity * sizeof(short), MemoryMappedFileAccess.ReadWrite); using MemoryMappedViewAccessor view = map.CreateViewAccessor(0, Header + Capacity * sizeof(short), MemoryMappedFileAccess.ReadWrite); if (view.ReadUInt32(0) != Magic || view.ReadUInt32(4) != Version) throw new InvalidDataException("Unsupported broadcast ring."); bool active = view.ReadUInt32(16) != 0; if (!active) { StopStream(); return; } VoiceCatClient owner = client ?? throw new IOException("Client disconnected."); if (streamId == 0) { StreamInfo stream = await owner.StartStreamAsync(StreamKind.StreamScreenAudio, "Screen audio", 2, token).ConfigureAwait(false); streamId = stream.StreamId; view.Write(32, view.ReadUInt64(24)); } ulong write = view.ReadUInt64(24), read = view.ReadUInt64(32); if (write - read > Capacity) read = write - Capacity; while (write - read >= Frame * 2) { for (int sample = 0; sample < scratch.Length; sample++) { ulong index = (read + (ulong)sample) % Capacity; scratch[sample] = view.ReadInt16(Header + checked((long)index * sizeof(short))); } if (!owner.Audio.FeedPcm(streamId, scratch, 2)) break; read += (ulong)scratch.Length; view.Write(32, read); } } private void StopStream() { uint id = streamId; streamId = 0; if (id == 0 || client?.State != ClientConnectionState.Connected) return; try { client.StopStream(id); } catch (Exception exception) when (exception is IOException or InvalidOperationException) { } } public async ValueTask DisposeAsync() { stop.Cancel(); try { await worker.ConfigureAwait(false); } catch (OperationCanceledException) { } StopStream(); stop.Dispose(); } }