如何在iOS上通过Speech SDK实现文本转语音流式播放?
解决方案:SPXPushAudioOutputStream 流式播放实现
核心思路
SPXPushAudioOutputStream 是推送式音频输出,AVPlayer 本身不支持直接喂原始数据流,你需要用更底层的音频播放框架处理实时数据:优先用 AVAudioEngine(Apple 现代音频框架),如果是 MP3 流则需要先解码为 PCM 再播放。
方案一:基于 AVAudioEngine 的 PCM 流式播放
Speech SDK 输出的 WAV 本质是 PCM 封装,直接处理 PCM 流是最简便的路径,步骤如下:
1. 初始化音频引擎组件
先创建 AVAudioEngine、AVAudioPlayerNode(负责播放),并配置和 Speech SDK 输出完全匹配的音频格式(必须一致,否则会无声/杂音):
import AVFoundation // 假设 Speech SDK 输出为 16bit、16kHz、单声道 PCM(请根据实际参数调整) let audioFormat = AVAudioFormat(commonFormat: .pcmFormatInt16, sampleRate: 16000, channels: 1, interleaved: true)! let audioEngine = AVAudioEngine() let playerNode = AVAudioPlayerNode() // 连接节点并启动引擎 audioEngine.attach(playerNode) audioEngine.connect(playerNode, to: audioEngine.mainMixerNode, format: audioFormat) try? audioEngine.start() playerNode.play()
2. 实时推送音频数据到播放器
实现 SPXPushAudioOutputStream 的数据回调,把拿到的 PCM 数据转换成 AVAudioPCMBuffer 后,交给 playerNode 播放:
// 示例:从 SPXPushAudioOutputStream 获取到的 Data 类型 PCM 数据 func handleAudioData(_ rawData: Data) { // 计算音频帧数量 let frameCount = UInt32(rawData.count) / audioFormat.streamDescription.pointee.mBytesPerFrame guard let audioBuffer = AVAudioPCMBuffer(pcmFormat: audioFormat, frameCapacity: frameCount) else { print("创建音频缓冲区失败") return } // 填充数据到缓冲区 audioBuffer.frameLength = frameCount rawData.copyBytes(to: audioBuffer.int16ChannelData![0], count: rawData.count) // 调度播放 playerNode.scheduleBuffer(audioBuffer, completionHandler: nil) }
方案二:MP3 流的处理(先解码再播放)
如果 Speech SDK 输出的是 MP3 压缩数据,需要先解码为 PCM 再用上述 AVAudioEngine 播放,推荐用 AudioFileStreamServices 做实时解码:
1. 初始化 MP3 流解码器
import AudioToolbox var audioFileStream: AudioFileStreamID? var targetFormat: AVAudioFormat? // 初始化流解码器回调 func setupMP3StreamDecoder() { var status = AudioFileStreamOpen(nil, streamPropertyCallback, streamPacketsCallback, kAudioFileMP3Type, &audioFileStream) guard status == noErr else { print("初始化 MP3 解码器失败") return } } // 回调:获取音频格式信息 func streamPropertyCallback(inClientData: UnsafeMutableRawPointer?, inFileStreamID: AudioFileStreamID, inPropertyID: AudioFileStreamPropertyID, ioFlags: UnsafeMutablePointer<AudioFileStreamPropertyFlags>?) { if inPropertyID == kAudioFileStreamProperty_DataFormat { var format = AudioStreamBasicDescription() var size = UInt32(MemoryLayout<AudioStreamBasicDescription>.size) AudioFileStreamGetProperty(inFileStreamID, inPropertyID, &size, &format) targetFormat = AVAudioFormat(streamDescription: &format) } } // 回调:获取解码后的 PCM 数据 func streamPacketsCallback(inClientData: UnsafeMutableRawPointer?, inFileStreamID: AudioFileStreamID, inNumberBytes: UInt32, inNumberPackets: UInt32, inInputData: UnsafeRawPointer, inPacketDescriptions: UnsafePointer<AudioStreamPacketDescription>?) { guard let format = targetFormat else { return } let pcmData = Data(bytes: inInputData, count: Int(inNumberBytes)) // 用方案一的 handleAudioData 方法播放 PCM 数据 handleAudioData(pcmData) }
2. 喂 MP3 数据到解码器
每次从 SPXPushAudioOutputStream 拿到 MP3 数据块,调用 AudioFileStreamParseBytes 进行解码:
func pushMP3Data(_ mp3Data: Data) { guard let stream = audioFileStream else { return } let bytes = mp3Data.withUnsafeBytes { $0.baseAddress! } AudioFileStreamParseBytes(stream, UInt32(mp3Data.count), bytes, 0) }
关键避坑点
- 音频格式严格匹配:Speech SDK 输出的采样率、位深、通道数必须和播放器/解码器的配置完全一致,这是无声/杂音的最常见原因
- 缓冲控制:如果数据推送速度快于播放速度,建议用环形缓冲区存储待播放数据,避免内存溢出
- 音频会话配置:记得激活播放会话,否则可能被系统静音:
let session = AVAudioSession.sharedInstance() try? session.setCategory(.playback, mode: .default) try? session.activate() - 错误捕获:AVAudioEngine 启动、播放可能抛出错误(比如音频设备被占用),要加入错误处理逻辑
内容的提问来源于stack exchange,提问作者keleko
相关产品推荐
相关产品推荐

