Files
suixinkan_uikit/suixinkan/Features/ScenicQueue/Services/AliyunPCMStreamingPlayer.swift

131 lines
4.4 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// 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<Int16>.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..<frameCount {
let low = UInt16(bytes[index * 2])
let high = UInt16(bytes[index * 2 + 1]) << 8
let sample = Int16(bitPattern: high | low)
channel[index] = Float(sample) / Float(Int16.max)
}
}
return buffer
}
}