You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 16:19:49