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

使用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,会导致缓冲区残留未初始化的垃圾数据,混入播放音频中产生噪音。

修复方案

  1. 维护TinySoundFont的播放时间轴:新增变量跟踪音频播放的采样位置,利用timestamp计算当前需要渲染的音频帧偏移,确保每次渲染的音频帧连续。
  2. 修正音频数据长度计算:生成Data时使用正确的字节数。
  3. 正确初始化音频缓冲区:用0初始化缓冲区,避免垃圾数据混入。
  4. 确保音频会话和引擎配置正确:启动引擎前激活音频会话,避免音频中断或配置不匹配。

修改后的代码

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.05 16:20:23