// // AliyunPCMStreamingPlayer.swift // suixinkan // import AVFoundation import Foundation /// TTS PCM 流式播放器协议,便于单元测试替换音频输出。 protocol AliyunPCMStreamingPlaying: AnyObject { /// 开始播放指定采样率的 16-bit mono PCM。 func start(sampleRate: Double) /// 写入一段 16-bit little-endian mono PCM 数据。 func write(_ data: Data) /// 通知播放器没有更多数据,已排空后回调。 func drain(_ completion: @escaping () -> Void) /// 立即停止并清空播放队列。 func stop() } /// 基于 AVAudioEngine 的 16-bit PCM 流式播放器。 final class AliyunPCMStreamingPlayer: AliyunPCMStreamingPlaying { private let stateQueue = DispatchQueue(label: "com.zhifly.suixinkan.aliyunNLS.pcmPlayer") private let engine = AVAudioEngine() private let playerNode = AVAudioPlayerNode() private var format: AVAudioFormat? private var scheduledBufferCount = 0 private var draining = false private var drainCompletion: (() -> Void)? private var started = false func start(sampleRate: Double) { stateQueue.sync { configureSession() if engine.attachedNodes.contains(playerNode) == false { engine.attach(playerNode) } let audioFormat = AVAudioFormat(commonFormat: .pcmFormatFloat32, sampleRate: sampleRate, channels: 1, interleaved: false) format = audioFormat if let audioFormat { engine.disconnectNodeOutput(playerNode) engine.connect(playerNode, to: engine.mainMixerNode, format: audioFormat) } if engine.isRunning == false { try? engine.start() } if playerNode.isPlaying == false { playerNode.play() } started = true draining = false drainCompletion = nil } } func write(_ data: Data) { guard data.count >= 2 else { return } stateQueue.async { guard self.started, let format = self.format else { return } let buffer = Self.makePCMBuffer(from: data, format: format) guard let buffer else { return } self.scheduledBufferCount += 1 self.playerNode.scheduleBuffer(buffer, completionCallbackType: .dataPlayedBack) { [weak self] _ in self?.stateQueue.async { guard let self else { return } self.scheduledBufferCount = max(0, self.scheduledBufferCount - 1) self.completeDrainIfNeeded() } } } } func drain(_ completion: @escaping () -> Void) { stateQueue.async { self.draining = true self.drainCompletion = completion self.completeDrainIfNeeded() } } func stop() { stateQueue.sync { playerNode.stop() engine.stop() scheduledBufferCount = 0 draining = false drainCompletion = nil started = false } } private func completeDrainIfNeeded() { guard draining, scheduledBufferCount == 0 else { return } let completion = drainCompletion drainCompletion = nil draining = false started = false playerNode.stop() completion?() } private func configureSession() { let session = AVAudioSession.sharedInstance() try? session.setCategory(.playback, options: [.allowBluetoothHFP, .mixWithOthers]) try? session.setActive(true) } private static func makePCMBuffer(from data: Data, format: AVAudioFormat) -> AVAudioPCMBuffer? { let frameCount = data.count / MemoryLayout.size guard frameCount > 0, let buffer = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: AVAudioFrameCount(frameCount)), let channel = buffer.floatChannelData?[0] else { return nil } buffer.frameLength = AVAudioFrameCount(frameCount) data.withUnsafeBytes { raw in let bytes = raw.bindMemory(to: UInt8.self) for index in 0..