集成阿里云 NUI 语音合成 SDK
This commit is contained in:
@ -0,0 +1,130 @@
|
||||
//
|
||||
// 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user