使用AVAudioSourceNode播放音频时的噪音问题排查与解决
watchOS下AVAudioSourceNode播放TinySoundFont音频出现噪音的问题修复
问题背景
在watchOS平台使用TinySoundFont处理SF2文件,期望通过AVAudioSourceNode实现实时播放——按下按钮时调用tsf_note_on,音频引擎需要新数据时调用tsf_render_short。目前将音频渲染到文件时正常,但通过AVAudioSourceNode播放时存在严重噪音。推测问题源于忽略了AVAudioSourceNode渲染回调中的timestamp参数,需要实时渲染方案以正确处理循环音效。
问题成因
- 忽略
timestamp参数的作用:AVAudioSourceNode的渲染回调提供的timestamp是音频引擎的精确时钟时间,TinySoundFont需要基于连续的时间轴生成音频帧。如果不利用这个时间戳维护播放进度,每次渲染回调都会从TinySoundFont的初始状态生成音频,导致前后帧数据不连续,产生断裂和噪音。 - 音频数据长度计算错误:原代码中
Data(bytes: storage, count: length)的count参数错误,Int16类型每个样本占2字节,正确长度应为length * MemoryLayout<Int16>.stride,否则会导致数据截断或读取垃圾数据。 - 缓冲区初始化问题:原代码用空数组初始化
storage,会导致缓冲区残留未初始化的垃圾数据,混入播放音频中产生噪音。
修复方案
- 维护TinySoundFont的播放时间轴:新增变量跟踪音频播放的采样位置,利用
timestamp计算当前需要渲染的音频帧偏移,确保每次渲染的音频帧连续。 - 修正音频数据长度计算:生成Data时使用正确的字节数。
- 正确初始化音频缓冲区:用0初始化缓冲区,避免垃圾数据混入。
- 确保音频会话和引擎配置正确:启动引擎前激活音频会话,避免音频中断或配置不匹配。
修改后的代码
ContentView.swift
import SwiftUI import AVFoundation struct ContentView: View { @ObservedObject var settings = Settings.shared init() { settings.prepare() } var body: some View { Button("Play Sound") { Settings.shared.playSound() if !settings.engine.isRunning { do { try settings.engine.start() } catch { print(error) } } } } }
Settings.swift
import SwiftUI import AVFoundation class Settings: ObservableObject { static let shared = Settings() var engine: AVAudioEngine! var sourceNode: AVAudioSourceNode! var tinySoundFont: OpaquePointer! private var currentSamplePosition: AVAudioFramePosition = 0 // 跟踪当前播放的采样位置 func prepare() { let soundFontPath = Bundle.main.path(forResource: "GMGSx", ofType: "sf2") tinySoundFont = tsf_load_filename(soundFontPath) tsf_set_output(tinySoundFont, TSF_MONO, 44100, 0) setUpSound() } func setUpSound() { if let engine = engine, let sourceNode = sourceNode { engine.detach(sourceNode) } engine = .init() let mixerNode = engine.mainMixerNode guard let audioFormat = AVAudioFormat( commonFormat: .pcmFormatInt16, sampleRate: 44100, channels: 1, interleaved: false ) else { return } sourceNode = AVAudioSourceNode(format: audioFormat) { [weak self] silence, timeStamp, frameCount, audioBufferList in guard let self = self, let timeStamp = timeStamp else { return 1 } // 计算当前需要渲染的采样偏移,确保时间轴连续 let targetSamplePosition = timeStamp.sampleTime let framesToRender = Int(frameCount) // 更新当前采样位置,避免累积误差 if self.currentSamplePosition != targetSamplePosition { self.currentSamplePosition = targetSamplePosition } guard let data = self.getSound(length: framesToRender) else { return 1 } let ablPointer = UnsafeMutableAudioBufferListPointer(audioBufferList) data.withUnsafeBytes { intPointer in let srcPointer = intPointer.baseAddress!.assumingMemoryBound(to: Int16.self) for buffer in ablPointer { let destPointer = buffer.mData!.assumingMemoryBound(to: Int16.self) memcpy(destPointer, srcPointer, framesToRender * MemoryLayout<Int16>.stride) } } self.currentSamplePosition += AVAudioFramePosition(framesToRender) return noErr } engine.attach(sourceNode) engine.connect(sourceNode, to: mixerNode, format: audioFormat) do { let session = AVAudioSession.sharedInstance() try session.setCategory(.playback) try session.activate() // 激活会话确保音频输出正常 } catch { print(error) } } func playSound() { tsf_note_on(tinySoundFont, 0, 60, 1) } func getSound(length: Int) -> Data? { // 分配缓冲区并初始化为0,避免垃圾数据 let storage = UnsafeMutablePointer<Int16>.allocate(capacity: length) storage.initialize(repeating: 0, count: length) // 渲染音频帧 tsf_render_short(tinySoundFont, storage, Int32(length), 0) // 生成正确长度的Data let data = Data(bytes: storage, count: length * MemoryLayout<Int16>.stride) storage.deallocate() return data } }
内容的提问来源于stack exchange,提问作者user3151675
相关产品推荐
相关产品推荐

