import CAudioRing import Foundation /// Producer side of one route: takes audio from any thread (NDI, input device) into a stereo ring buffer. /// /// One feed has exactly one producer and one consumer (the route on the render thread). public final class AudioFeed: @unchecked Sendable { let ring: OpaquePointer /// Sample rate of the audio in the ring. Written by the producer, read by the render thread. private(set) var sampleRate: Double = 0 // Producer-only scratch for interleaving. private var scratch: UnsafeMutablePointer private var scratchFrames = 0 public init(capacitySeconds: Double = 1, maxSampleRate: Double = 96_000) { ring = musubi_ring_create(UInt32(capacitySeconds * maxSampleRate), 2)! scratchFrames = 4096 scratch = .allocate(capacity: scratchFrames * 2) } deinit { musubi_ring_destroy(ring) scratch.deallocate() } /// Pushes planar audio. Mono is duplicated to both channels; channels beyond two are ignored. public func push(planar data: UnsafePointer, channelStride: Int, channels: Int, frames: Int, sampleRate: Double) { guard channels > 0, frames > 0 else { return } self.sampleRate = sampleRate ensureScratch(frames) let left = data let right = channels > 1 ? data + channelStride : data for i in 0.., frames: Int) -> Int { Int(musubi_ring_read(ring, interleaved, UInt32(frames))) } /// Sample rate of the most recent push (0 before the first). public var currentSampleRate: Double { sampleRate } private func ensureScratch(_ frames: Int) { guard frames > scratchFrames else { return } scratch.deallocate() scratchFrames = frames scratch = .allocate(capacity: frames * 2) } }