avfoundationavaudioconverter

Trying to convert format of a file using AVAudioConverter works but includes garbage


I am trying to convert a file to a different format using AVAudioConverter.

Using ChatGPT to guide me, as I haven't worked with audio before, I came up with the code below. It's the closest I have gotten, and while it creates the file with my voice , the file is slower and has noise.

Can't seem to get any further to nail down where I am going wrong, or what is missing.

sample wav file

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")


                    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 readingError: Error? = nil // To track potential read errors
                    while readingError == nil { // Loop as long as no read error occurred
                        let inputBuffer = AVAudioPCMBuffer(pcmFormat: inputFile.processingFormat, frameCapacity: 1024)!
                        do {
                            try inputFile.read(into: inputBuffer)
                            if inputBuffer.frameLength == 0 {
                                break // EOF reached successfully
                            }

                            let outputBuffer = AVAudioPCMBuffer(pcmFormat: outputFile.processingFormat, frameCapacity: 1024)!

                            let inputBlock: AVAudioConverterInputBlock = { _, outStatus in
                                outStatus.pointee = .haveData
                                return inputBuffer
                            }

                            try converter.convert(to: outputBuffer, error: nil, withInputFrom: inputBlock)

                            try outputFile.write(from: outputBuffer)

                        } catch {
                            print("Error during audio reading: \(error.localizedDescription)")
                            readingError = error // Set the error to exit the loop
                        }
                    }

                    print("Conversion complete")

                    // Get and display the output file's format
                    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)")
                }
            }
        }
}

Solution

  • Your AVAudioConverterInputBlock is returning the same buffer over and over which creates a funky, but undesirable effect. You need to return each buffer only once, like this:

    let outputBuffer = AVAudioPCMBuffer(pcmFormat: outputFile.processingFormat, frameCapacity: 1024)!
    var haveData = true
    
    let inputBlock: AVAudioConverterInputBlock = { _, outStatus in
        if haveData {
            outStatus.pointee = .haveData
            haveData = false
            return inputBuffer
        } else {
            outStatus.pointee = .noDataNow
            return nil
        }
    }