对齐 Android 无权限强制弹窗、角色下拉与申请页交互,补充 UIButton configuration 适配,并增强景区排队 TTS 与相关单元测试。 Co-authored-by: Cursor <cursoragent@cursor.com>
586 lines
21 KiB
Swift
586 lines
21 KiB
Swift
//
|
||
// ScenicQueueTTSManager.swift
|
||
// suixinkan
|
||
//
|
||
|
||
import AVFoundation
|
||
import Foundation
|
||
|
||
/// 排队语音播报服务协议,隔离具体 TTS SDK。
|
||
protocol ScenicQueueTTSManaging: AnyObject {
|
||
/// 自定义文案是否正在循环播报。
|
||
var customTextLooping: Bool { get }
|
||
|
||
/// 是否支持离线语音合成。
|
||
var supportsOfflineTTS: Bool { get }
|
||
|
||
/// 预热排队 TTS 引擎。
|
||
func prepareQueueTTSEngine(api: ScenicQueueAPIProtocol?) async
|
||
|
||
/// 开始循环播报指定排号。
|
||
func startQueueCallLoop(queueNo: String)
|
||
|
||
/// 停止叫号循环。
|
||
func stopQueueCallLoop()
|
||
|
||
/// 单次播报普通文本。
|
||
@discardableResult
|
||
func speakPlainTextOnce(_ text: String) async -> Bool
|
||
|
||
/// 开始循环播报自定义文本。
|
||
@discardableResult
|
||
func startCustomTextLoop(_ text: String) async -> Bool
|
||
|
||
/// 停止自定义文本循环。
|
||
func stopCustomTextLoop()
|
||
|
||
/// 中断拍摄倒计时等临时播报。
|
||
func interruptShootingAnnouncement()
|
||
|
||
/// 估算播报耗时。
|
||
func estimateAnnouncementDurationMillis(_ text: String) -> Int64
|
||
}
|
||
|
||
/// 阿里云 NLS TTS 管理器,优先使用 NUI SDK,失败时回退到系统中文语音。
|
||
final class AliyunNLSTTSManager: NSObject, ScenicQueueTTSManaging, AVSpeechSynthesizerDelegate {
|
||
static let shared = AliyunNLSTTSManager()
|
||
|
||
private enum LoopKind {
|
||
case queueCall(String)
|
||
case custom(String)
|
||
}
|
||
|
||
/// 当前已初始化的阿里云原生 TTS 引擎类型。
|
||
private enum NativeEngineKind {
|
||
case online
|
||
case offline
|
||
}
|
||
|
||
/// 离线 TTS 工作目录及默认发音人资源。
|
||
private struct OfflineResources {
|
||
let workspace: String
|
||
let voicePath: String
|
||
}
|
||
|
||
private let synthesizer = AVSpeechSynthesizer()
|
||
private let loopQueue = DispatchQueue(label: "com.zhifly.suixinkan.scenicQueue.tts")
|
||
private let nativeQueue = DispatchQueue(label: "com.zhifly.suixinkan.scenicQueue.aliyunNLS")
|
||
private let appStore: AppStore
|
||
private let tokenProvider: AliyunNLSTokenProviding
|
||
private let bridge: AliyunNuiTTSBridging
|
||
private let streamingPlayer: AliyunPCMStreamingPlaying
|
||
private let workspacePathProvider: () -> String?
|
||
private var activeLoop: LoopKind?
|
||
private var currentContinuation: CheckedContinuation<Void, Never>?
|
||
private var nativeContinuation: CheckedContinuation<Bool, Never>?
|
||
private var nlsToken: AliyunNLSToken?
|
||
private var lastCredentials: AliyunOSSCredentials?
|
||
private var nativeReady = false
|
||
private var nativeEngineKind: NativeEngineKind?
|
||
private var nativeSampleRate: Double
|
||
|
||
private(set) var customTextLooping = false
|
||
|
||
var supportsOfflineTTS: Bool {
|
||
bridge.sdkAvailable && offlineResources() != nil
|
||
}
|
||
|
||
init(
|
||
appStore: AppStore = .shared,
|
||
tokenProvider: AliyunNLSTokenProviding = AliyunNLSTokenProvider(),
|
||
bridge: AliyunNuiTTSBridging = AliyunNuiTTSBridgeAdapter(),
|
||
streamingPlayer: AliyunPCMStreamingPlaying = AliyunPCMStreamingPlayer(),
|
||
workspacePathProvider: (() -> String?)? = nil
|
||
) {
|
||
self.appStore = appStore
|
||
self.tokenProvider = tokenProvider
|
||
self.bridge = bridge
|
||
self.streamingPlayer = streamingPlayer
|
||
self.workspacePathProvider = workspacePathProvider ?? Self.bundledWorkspacePath
|
||
nativeSampleRate = AliyunNLSTTSManager.onlineSampleRate
|
||
super.init()
|
||
synthesizer.delegate = self
|
||
self.bridge.delegate = self
|
||
}
|
||
|
||
func prepareQueueTTSEngine(api: ScenicQueueAPIProtocol?) async {
|
||
guard let api else { return }
|
||
guard !appStore.scenicQueue.useOfflineTTS || supportsOfflineTTS else {
|
||
nativeReady = false
|
||
return
|
||
}
|
||
do {
|
||
let sts = try await api.aliyunSTS(bucket: "vipsky")
|
||
lastCredentials = sts.credentials
|
||
let token = try await tokenProvider.createToken(credentials: sts.credentials)
|
||
nlsToken = token
|
||
nativeReady = await prepareNativeEngineIfAvailable(token: token, credentials: sts.credentials)
|
||
} catch {
|
||
nativeReady = false
|
||
}
|
||
}
|
||
|
||
func startQueueCallLoop(queueNo: String) {
|
||
let text = Self.callAnnouncementText(queueNo: queueNo)
|
||
loopQueue.async { [weak self] in
|
||
guard let self else { return }
|
||
self.activeLoop = .queueCall(queueNo)
|
||
self.customTextLooping = false
|
||
Task { await self.runLoop(text: text, expected: .queueCall(queueNo)) }
|
||
}
|
||
}
|
||
|
||
func stopQueueCallLoop() {
|
||
loopQueue.async { [weak self] in
|
||
guard let self else { return }
|
||
if case .queueCall = self.activeLoop {
|
||
self.activeLoop = nil
|
||
}
|
||
self.stopSpeaking()
|
||
}
|
||
}
|
||
|
||
@discardableResult
|
||
func speakPlainTextOnce(_ text: String) async -> Bool {
|
||
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
|
||
guard !trimmed.isEmpty else { return false }
|
||
await speak(trimmed)
|
||
return true
|
||
}
|
||
|
||
@discardableResult
|
||
func startCustomTextLoop(_ text: String) async -> Bool {
|
||
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
|
||
guard !trimmed.isEmpty else { return false }
|
||
loopQueue.async { [weak self] in
|
||
guard let self else { return }
|
||
self.activeLoop = .custom(trimmed)
|
||
self.customTextLooping = true
|
||
Task { await self.runLoop(text: trimmed, expected: .custom(trimmed)) }
|
||
}
|
||
return true
|
||
}
|
||
|
||
func stopCustomTextLoop() {
|
||
loopQueue.async { [weak self] in
|
||
guard let self else { return }
|
||
if case .custom = self.activeLoop {
|
||
self.activeLoop = nil
|
||
}
|
||
self.customTextLooping = false
|
||
self.stopSpeaking()
|
||
}
|
||
}
|
||
|
||
func interruptShootingAnnouncement() {
|
||
stopSpeaking()
|
||
}
|
||
|
||
func estimateAnnouncementDurationMillis(_ text: String) -> Int64 {
|
||
let count = max(1, text.count)
|
||
return Int64(min(max(850 + count * 200, 900), 12_000))
|
||
}
|
||
|
||
/// 排队号 TTS 格式化,降低字母数字连读概率。
|
||
static func formatQueueNoForTTS(_ queueNo: String) -> String {
|
||
let trimmed = queueNo.trimmingCharacters(in: .whitespacesAndNewlines)
|
||
let core = trimmed.hasSuffix("号") ? String(trimmed.dropLast()) : trimmed
|
||
guard !core.isEmpty else { return trimmed.isEmpty ? "请" : trimmed }
|
||
let pattern = #"^([A-Za-z]+)([0-9]+)$"#
|
||
if let regex = try? NSRegularExpression(pattern: pattern),
|
||
let match = regex.firstMatch(in: core, range: NSRange(core.startIndex..., in: core)),
|
||
let lettersRange = Range(match.range(at: 1), in: core),
|
||
let digitsRange = Range(match.range(at: 2), in: core) {
|
||
return "\(core[lettersRange].uppercased()),\(core[digitsRange])号"
|
||
}
|
||
return "\(core)号"
|
||
}
|
||
|
||
/// 叫号播报文案。
|
||
static func callAnnouncementText(queueNo: String) -> String {
|
||
"\(formatQueueNoForTTS(queueNo)),请到拍摄区域,准备拍摄。"
|
||
}
|
||
|
||
private func runLoop(text: String, expected: LoopKind) async {
|
||
while isLoopStillActive(expected) {
|
||
await speak(text)
|
||
guard isLoopStillActive(expected) else { break }
|
||
try? await Task.sleep(nanoseconds: 1_000_000_000)
|
||
}
|
||
if case .custom = expected {
|
||
customTextLooping = false
|
||
}
|
||
}
|
||
|
||
private func isLoopStillActive(_ expected: LoopKind) -> Bool {
|
||
loopQueue.sync {
|
||
switch (activeLoop, expected) {
|
||
case (.queueCall(let lhs), .queueCall(let rhs)):
|
||
lhs == rhs
|
||
case (.custom(let lhs), .custom(let rhs)):
|
||
lhs == rhs
|
||
default:
|
||
false
|
||
}
|
||
}
|
||
}
|
||
|
||
private func speak(_ text: String) async {
|
||
if await speakWithNativeEngine(text) {
|
||
return
|
||
}
|
||
await speakWithSystemVoice(text)
|
||
}
|
||
|
||
private func speakWithNativeEngine(_ text: String) async -> Bool {
|
||
await withCheckedContinuation { continuation in
|
||
nativeQueue.async { [weak self] in
|
||
guard let self, self.nativeReady, self.bridge.sdkAvailable else {
|
||
continuation.resume(returning: false)
|
||
return
|
||
}
|
||
self.cancelNativeSpeakLocked(resumeExisting: true)
|
||
self.nativeContinuation = continuation
|
||
self.streamingPlayer.stop()
|
||
self.streamingPlayer.start(sampleRate: self.nativeSampleRate)
|
||
self.applyNativePlaybackParams(text: text)
|
||
let code = self.bridge.play(priority: "1", taskId: Self.nuiTaskId, text: text)
|
||
if code != 0 {
|
||
self.streamingPlayer.stop()
|
||
self.nativeContinuation = nil
|
||
continuation.resume(returning: false)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
private func speakWithSystemVoice(_ text: String) async {
|
||
await MainActor.run {
|
||
if synthesizer.isSpeaking {
|
||
synthesizer.stopSpeaking(at: .immediate)
|
||
}
|
||
}
|
||
await withCheckedContinuation { continuation in
|
||
currentContinuation = continuation
|
||
let utterance = AVSpeechUtterance(string: text)
|
||
utterance.voice = AVSpeechSynthesisVoice(language: "zh-CN")
|
||
utterance.rate = AVSpeechUtteranceDefaultSpeechRate * 0.86
|
||
utterance.volume = 1.0
|
||
DispatchQueue.main.async {
|
||
self.synthesizer.speak(utterance)
|
||
}
|
||
}
|
||
}
|
||
|
||
private func stopSpeaking() {
|
||
nativeQueue.async { [weak self] in
|
||
self?.cancelNativeSpeakLocked(resumeExisting: true)
|
||
}
|
||
DispatchQueue.main.async {
|
||
if self.synthesizer.isSpeaking {
|
||
self.synthesizer.stopSpeaking(at: .immediate)
|
||
}
|
||
self.resumeCurrentContinuationIfNeeded()
|
||
}
|
||
}
|
||
|
||
private func resumeCurrentContinuationIfNeeded() {
|
||
currentContinuation?.resume()
|
||
currentContinuation = nil
|
||
}
|
||
|
||
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance) {
|
||
resumeCurrentContinuationIfNeeded()
|
||
}
|
||
|
||
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didCancel utterance: AVSpeechUtterance) {
|
||
resumeCurrentContinuationIfNeeded()
|
||
}
|
||
|
||
private func prepareNativeEngineIfAvailable(
|
||
token: AliyunNLSToken,
|
||
credentials: AliyunOSSCredentials
|
||
) async -> Bool {
|
||
guard bridge.sdkAvailable else {
|
||
return false
|
||
}
|
||
let wantsOffline = appStore.scenicQueue.useOfflineTTS
|
||
let offlineResources = wantsOffline ? offlineResources() : nil
|
||
guard !wantsOffline || offlineResources != nil else { return false }
|
||
guard let ticket = wantsOffline
|
||
? nativeOfflineTicket(token: token.id, credentials: credentials, resources: offlineResources!)
|
||
: nativeOnlineTicket(token: token.id) else { return false }
|
||
|
||
return await withCheckedContinuation { continuation in
|
||
nativeQueue.async { [weak self] in
|
||
guard let self else {
|
||
continuation.resume(returning: false)
|
||
return
|
||
}
|
||
self.cancelNativeSpeakLocked(resumeExisting: true)
|
||
_ = self.bridge.releaseEngine()
|
||
self.nativeEngineKind = nil
|
||
let code = self.bridge.initialize(ticket: ticket, logLevel: 0, saveLog: true)
|
||
guard code == 0 else {
|
||
continuation.resume(returning: false)
|
||
return
|
||
}
|
||
let configured: Bool
|
||
if let offlineResources {
|
||
configured = self.configureOfflineEngine(resources: offlineResources)
|
||
} else {
|
||
self.nativeEngineKind = .online
|
||
self.nativeSampleRate = Self.onlineSampleRate
|
||
self.applyNativeOnlineQueueProsodyParams()
|
||
configured = true
|
||
}
|
||
guard configured else {
|
||
_ = self.bridge.releaseEngine()
|
||
self.nativeEngineKind = nil
|
||
continuation.resume(returning: false)
|
||
return
|
||
}
|
||
continuation.resume(returning: true)
|
||
}
|
||
}
|
||
}
|
||
|
||
private func nativeOnlineTicket(token: String) -> String? {
|
||
guard let workspace = workspacePathProvider() else { return nil }
|
||
let ticket: [String: String] = [
|
||
"app_key": Self.onlineAppKey,
|
||
"token": token,
|
||
"workspace": workspace,
|
||
"debug_path": nativeDebugPath(),
|
||
"url": "wss://nls-gateway.cn-shanghai.aliyuncs.com:443/ws/v1",
|
||
"device_id": Self.deviceId(),
|
||
"mode_type": "2",
|
||
"tts_version": "0",
|
||
]
|
||
guard let data = try? JSONSerialization.data(withJSONObject: ticket) else { return nil }
|
||
return String(data: data, encoding: .utf8)
|
||
}
|
||
|
||
private func nativeOfflineTicket(
|
||
token: String,
|
||
credentials: AliyunOSSCredentials,
|
||
resources: OfflineResources
|
||
) -> String? {
|
||
let ticket: [String: String] = [
|
||
"app_key": Self.offlineAppKey,
|
||
"ak_id": credentials.accessKeyId,
|
||
"ak_secret": credentials.accessKeySecret,
|
||
"sts_token": credentials.securityToken,
|
||
"token": token,
|
||
"sdk_code": Self.offlineSDKCode,
|
||
"save_wav": "false",
|
||
"debug_path": nativeDebugPath(),
|
||
"workspace": resources.workspace,
|
||
"log_track_level": "5",
|
||
"mode_type": "0",
|
||
"device_id": Self.deviceId(),
|
||
]
|
||
guard let data = try? JSONSerialization.data(withJSONObject: ticket) else { return nil }
|
||
return String(data: data, encoding: .utf8)
|
||
}
|
||
|
||
private static func bundledWorkspacePath() -> String? {
|
||
if let privateFrameworksPath = Bundle.main.privateFrameworksPath {
|
||
let frameworkPath = (privateFrameworksPath as NSString).appendingPathComponent("nuisdk.framework")
|
||
if let frameworkBundle = Bundle(path: frameworkPath),
|
||
let path = frameworkBundle.path(forResource: "Resources", ofType: "bundle") {
|
||
return path
|
||
}
|
||
}
|
||
return Bundle.main.path(forResource: "Resources", ofType: "bundle")
|
||
}
|
||
|
||
private func offlineResources() -> OfflineResources? {
|
||
guard let workspace = workspacePathProvider() else { return nil }
|
||
let ttsPath = (workspace as NSString).appendingPathComponent("tts")
|
||
let languagePath = (ttsPath as NSString).appendingPathComponent("languagedata_embedded.bin")
|
||
let parameterPath = (ttsPath as NSString).appendingPathComponent("parameter.cfg")
|
||
let voicePath = (ttsPath as NSString).appendingPathComponent("voices/\(Self.offlineVoiceName)")
|
||
let fileManager = FileManager.default
|
||
guard fileManager.fileExists(atPath: languagePath),
|
||
fileManager.fileExists(atPath: parameterPath),
|
||
fileManager.fileExists(atPath: voicePath) else { return nil }
|
||
return OfflineResources(workspace: workspace, voicePath: voicePath)
|
||
}
|
||
|
||
private func nativeDebugPath() -> String {
|
||
let caches = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first
|
||
let url = (caches ?? URL(fileURLWithPath: NSTemporaryDirectory())).appendingPathComponent("aliyun_nls_tts_debug", isDirectory: true)
|
||
try? FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
|
||
return url.path
|
||
}
|
||
|
||
private func applyNativePlaybackParams(text: String) {
|
||
switch nativeEngineKind {
|
||
case .online:
|
||
_ = bridge.setParam("mode_type", value: "2")
|
||
_ = bridge.setParam("tts_version", value: text.count > 300 ? "1" : "0")
|
||
applyNativeOnlineQueueProsodyParams()
|
||
case .offline:
|
||
applyNativeOfflineQueueProsodyParams()
|
||
case nil:
|
||
break
|
||
}
|
||
}
|
||
|
||
private func applyNativeOnlineQueueProsodyParams() {
|
||
_ = bridge.setParam("speech_rate", value: Self.onlineSpeechRate)
|
||
_ = bridge.setParam("volume", value: Self.playbackVolume)
|
||
}
|
||
|
||
private func configureOfflineEngine(resources: OfflineResources) -> Bool {
|
||
guard bridge.setParam("extend_font_name", value: resources.voicePath) == 0 else { return false }
|
||
guard bridge.setParam("encode_type", value: "pcm") == 0 else { return false }
|
||
guard bridge.setParam("sample_rate", value: Self.offlineSampleRateText) == 0 else { return false }
|
||
nativeEngineKind = .offline
|
||
nativeSampleRate = Double(bridge.paramValue(for: "model_sample_rate") ?? "")
|
||
.flatMap { $0 > 0 ? $0 : nil } ?? Self.offlineSampleRate
|
||
applyNativeOfflineQueueProsodyParams()
|
||
return true
|
||
}
|
||
|
||
private func applyNativeOfflineQueueProsodyParams() {
|
||
_ = bridge.setParam("speed_level", value: Self.offlineSpeedLevel)
|
||
_ = bridge.setParam("volume", value: Self.playbackVolume)
|
||
}
|
||
|
||
private func cancelNativeSpeakLocked(resumeExisting: Bool) {
|
||
_ = bridge.cancel(taskId: Self.nuiTaskId)
|
||
streamingPlayer.stop()
|
||
if resumeExisting {
|
||
finishNativeSpeakLocked(success: false)
|
||
} else {
|
||
nativeContinuation = nil
|
||
}
|
||
}
|
||
|
||
private func finishNativeSpeakLocked(success: Bool) {
|
||
let continuation = nativeContinuation
|
||
nativeContinuation = nil
|
||
continuation?.resume(returning: success)
|
||
}
|
||
|
||
private static func deviceId() -> String {
|
||
let key = "aliyun_nls_tts_device_id_v1"
|
||
if let value = UserDefaults.standard.string(forKey: key), !value.isEmpty {
|
||
return value
|
||
}
|
||
let value = UUID().uuidString
|
||
UserDefaults.standard.set(value, forKey: key)
|
||
return value
|
||
}
|
||
|
||
private static let onlineAppKey = "3WX8cD04JCGkbRLi"
|
||
private static let offlineAppKey = "gfWimmOuTku1AbIV"
|
||
private static let offlineSDKCode = "software_nls_tts_offline_standard"
|
||
private static let offlineVoiceName = "aijia"
|
||
private static let nuiTaskId = "1"
|
||
private static let onlineSampleRate = 16_000.0
|
||
private static let offlineSampleRate = 24_000.0
|
||
private static let offlineSampleRateText = "24000"
|
||
private static let onlineSpeechRate = "-120"
|
||
private static let offlineSpeedLevel = "1.0"
|
||
private static let playbackVolume = "2.0"
|
||
}
|
||
|
||
/// 阿里云 NUI TTS Swift 桥接协议。
|
||
protocol AliyunNuiTTSBridging: AnyObject {
|
||
/// SDK 回调代理。
|
||
var delegate: AliyunNuiTTSBridgeDelegate? { get set }
|
||
|
||
/// 当前平台是否可使用 SDK。
|
||
var sdkAvailable: Bool { get }
|
||
|
||
/// 初始化引擎。
|
||
func initialize(ticket: String, logLevel: Int, saveLog: Bool) -> Int
|
||
|
||
/// 设置参数。
|
||
func setParam(_ param: String, value: String) -> Int
|
||
|
||
/// 开始播放。
|
||
func play(priority: String, taskId: String, text: String) -> Int
|
||
|
||
/// 取消任务。
|
||
func cancel(taskId: String?) -> Int
|
||
|
||
/// 释放引擎。
|
||
func releaseEngine() -> Int
|
||
|
||
/// 读取 SDK 参数。
|
||
func paramValue(for key: String) -> String?
|
||
}
|
||
|
||
/// 阿里云 NUI TTS Objective-C 桥接器的 Swift 适配器。
|
||
final class AliyunNuiTTSBridgeAdapter: AliyunNuiTTSBridging {
|
||
private let bridge: AliyunNuiTTSBridge
|
||
|
||
var delegate: AliyunNuiTTSBridgeDelegate? {
|
||
get { bridge.delegate }
|
||
set { bridge.delegate = newValue }
|
||
}
|
||
|
||
var sdkAvailable: Bool { bridge.sdkAvailable }
|
||
|
||
init(bridge: AliyunNuiTTSBridge = AliyunNuiTTSBridge()) {
|
||
self.bridge = bridge
|
||
}
|
||
|
||
func initialize(ticket: String, logLevel: Int, saveLog: Bool) -> Int {
|
||
bridge.initialize(withTicket: ticket, logLevel: logLevel, saveLog: saveLog)
|
||
}
|
||
|
||
func setParam(_ param: String, value: String) -> Int {
|
||
bridge.setParam(param, value: value)
|
||
}
|
||
|
||
func play(priority: String, taskId: String, text: String) -> Int {
|
||
bridge.play(withPriority: priority, taskId: taskId, text: text)
|
||
}
|
||
|
||
func cancel(taskId: String?) -> Int {
|
||
bridge.cancel(withTaskId: taskId)
|
||
}
|
||
|
||
func releaseEngine() -> Int {
|
||
bridge.releaseEngine()
|
||
}
|
||
|
||
func paramValue(for key: String) -> String? {
|
||
bridge.paramValue(forKey: key)
|
||
}
|
||
}
|
||
|
||
extension AliyunNLSTTSManager: AliyunNuiTTSBridgeDelegate {
|
||
func aliyunNuiTTSBridge(_ bridge: AliyunNuiTTSBridge, didReceiveEvent event: Int, taskId: String?, code: Int) {
|
||
nativeQueue.async { [weak self] in
|
||
guard let self else { return }
|
||
switch event {
|
||
case 1:
|
||
self.streamingPlayer.drain { [weak self] in
|
||
self?.nativeQueue.async {
|
||
self?.finishNativeSpeakLocked(success: true)
|
||
}
|
||
}
|
||
case 2, 5:
|
||
self.streamingPlayer.stop()
|
||
self.finishNativeSpeakLocked(success: false)
|
||
default:
|
||
break
|
||
}
|
||
_ = taskId
|
||
_ = code
|
||
}
|
||
}
|
||
|
||
func aliyunNuiTTSBridge(_ bridge: AliyunNuiTTSBridge, didReceiveAudioData data: Data, taskId: String?, info: String?) {
|
||
streamingPlayer.write(data)
|
||
_ = taskId
|
||
_ = info
|
||
}
|
||
}
|