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

使用AVAudioConverter转换音频出现杂音且播放缓慢问题求助

问题修复:AVAudioConverter音频转换后播放缓慢、有杂音

你的代码在使用AVAudioConverter进行采样率和格式转换时,存在两个核心错误,直接导致了播放缓慢、杂音问题:

  1. 输出缓冲区容量固定为1024帧,未考虑采样率转换后的帧数量变化,引发数据截断或重复
  2. 输入块逻辑错误,反复返回同一个输入缓冲区,未正确处理转换器的数据流需求

修复后的完整代码

import SwiftUI
import AVFoundation

struct ConvertAudio: View {
    @State var inputFormat        = "\n\n\n"
    @State var outputFormat       = "\n\n\n"
    @State var documentsDirectory : URL?

    var body: some View {
        ZStack {
            Color.black
                .ignoresSafeArea()

            VStack{
                Spacer()
                Text(inputFormat)
                    .foregroundStyle(.yellow)

                Spacer()

                Button {
                    convert()
                } label: {
                    Text("Convert 44 to 16")
                        .font(.title)
                        .foregroundStyle(.white)
                        .frame(width: 300, height: 75)
                        .background(.gray)
                        .cornerRadius(10)
                }

                Spacer()

                Text(outputFormat)
                    .foregroundStyle(.green)

                Spacer()
            }
        }
        .onAppear{
            documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
        }
    }

    func convert() {
        guard let inputUrl = Bundle.main.url(forResource: "44input", withExtension: "wav") else { return }

        Task {
            let outputSettings: [String: Any] = [
                AVFormatIDKey: kAudioFormatLinearPCM,
                AVSampleRateKey: 16000.0,
                AVNumberOfChannelsKey: 1,
                AVLinearPCMBitDepthKey: 16,
                AVLinearPCMIsNonInterleaved: false,
                AVLinearPCMIsFloatKey: false,
                AVLinearPCMIsBigEndianKey: false
            ]

            // 读取输入格式信息
            let asset = AVURLAsset(url: inputUrl)
            do {
                let tracks = try await asset.load(.tracks)
                if let track = tracks.first(where: { $0.mediaType == .audio }) {
                    let formatDescriptions = try await track.load(.formatDescriptions)
                    if let formatDescription = formatDescriptions.first,
                       let asbdPointer = CMAudioFormatDescriptionGetStreamBasicDescription(formatDescription) {
                        let asbd = asbdPointer.pointee
                        inputFormat = "Sample Rate: \(asbd.mSampleRate)\n" +
                                      "Channels: \(asbd.mChannelsPerFrame)\n" +
                                      "Format ID: \(asbd.mFormatID)\n" +
                                      "Bits per Channel: \(asbd.mBitsPerChannel)"
                    }
                }
            } catch {
                print("Error loading asset format: \(error.localizedDescription)")
                return
            }

            do {
                let inputFile = try AVAudioFile(forReading: inputUrl)
                guard let documentsDirectory else { return }
                let outputURL = documentsDirectory.appendingPathComponent("16test.wav")
                
                // 删除已存在的输出文件,避免追加写入
                if FileManager.default.fileExists(atPath: outputURL.path) {
                    try FileManager.default.removeItem(at: outputURL)
                }
                
                let outputFile = try AVAudioFile(forWriting: outputURL, settings: outputSettings)
                guard let converter = AVAudioConverter(from: inputFile.processingFormat, to: outputFile.processingFormat) else {
                    print("Failed to create converter")
                    return
                }

                var isInputFinished = false
                let inputBlock: AVAudioConverterInputBlock = { _, outStatus in
                    guard !isInputFinished else {
                        outStatus.pointee = .endOfData
                        return nil
                    }

                    let inputBuffer = AVAudioPCMBuffer(pcmFormat: inputFile.processingFormat, frameCapacity: 1024)!
                    do {
                        try inputFile.read(into: inputBuffer)
                        if inputBuffer.frameLength == 0 {
                            isInputFinished = true
                            outStatus.pointee = .endOfData
                            return nil
                        }
                        outStatus.pointee = .haveData
                        return inputBuffer
                    } catch {
                        print("Error reading input audio: \(error.localizedDescription)")
                        isInputFinished = true
                        outStatus.pointee = .endOfData
                        return nil
                    }
                }

                // 循环处理转换,直到输入结束
                while !isInputFinished {
                    // 动态计算所需输出缓冲区容量
                    let inputFrameCount = 1024
                    let outputFrameCount = converter.requiredOutputFrameCount(forInputFrameCount: AVAudioFrameCount(inputFrameCount))
                    guard let outputBuffer = AVAudioPCMBuffer(pcmFormat: outputFile.processingFormat, frameCapacity: outputFrameCount) else {
                        print("Failed to create output buffer")
                        break
                    }

                    var error: NSError?
                    let status = converter.convert(to: outputBuffer, error: &error, withInputFrom: inputBlock)
                    
                    switch status {
                    case .haveData:
                        try outputFile.write(from: outputBuffer)
                    case .endOfData:
                        isInputFinished = true
                    case .error:
                        if let error = error {
                            print("Conversion error: \(error.localizedDescription)")
                        }
                        break
                    default:
                        break
                    }
                }

                print("Conversion complete")

                // 读取输出格式信息
                let outputAsset = AVURLAsset(url: outputURL)
                do {
                    let tracks = try await outputAsset.load(.tracks)
                    if let track = tracks.first(where: { $0.mediaType == .audio }) {
                        let formatDescriptions = try await track.load(.formatDescriptions)
                        if let formatDescription = formatDescriptions.first,
                           let asbdPointer = CMAudioFormatDescriptionGetStreamBasicDescription(formatDescription) {
                            let asbd = asbdPointer.pointee
                            outputFormat = "Sample Rate: \(asbd.mSampleRate)\n" +
                                           "Channels: \(asbd.mChannelsPerFrame)\n" +
                                           "Format ID: \(asbd.mFormatID)\n" +
                                           "Bits per Channel: \(asbd.mBitsPerChannel)"
                        }
                    }
                } catch {
                    print("Error loading output asset format: \(error.localizedDescription)")
                }

            } catch {
                print("Audio conversion error: \(error.localizedDescription)")
            }
        }
    }
}

关键改动说明

  1. 动态计算输出缓冲区容量:
    使用converter.requiredOutputFrameCount(forInputFrameCount:)根据输入帧数量计算转换后的输出帧数量,确保缓冲区能完全容纳转换后的数据,避免截断或重复。

  2. 修正输入块逻辑:
    将输入块移到循环外,统一处理数据读取和状态标记,在输入结束时返回.endOfData状态,让转换器正确识别数据流终点。

  3. 处理文件覆盖:
    在创建输出文件前删除已存在的同名文件,避免追加写入导致的音频异常。

  4. 完善转换状态处理:
    对convert方法的返回状态进行判断,分别处理有数据、结束、错误三种情况,确保转换流程稳定。

内容的提问来源于stack exchange,提问作者Bartender1382

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 03:57:32