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

如何使用AVAssetWriter导出MP3文件?解决无效文件类型UTI错误

解决macOS 10.15下AVAssetWriter无法导出MP3的问题

我完全懂你的惊讶——在macOS 10.15上用AVAssetWriter导出MP3居然报错,系统给出的支持格式列表里确实没有MP3的身影,这确实挺让人意外的。别着急,咱们来一步步解决这个问题:

问题根源

你遇到的错误已经说得很明确:AVAssetWriter本身就不支持AVFileType.mp3这个UTI。Apple在AVFoundation的这个组件中没有集成MP3编码器,所以直接用它创建MP3输出实例肯定会失败。

可行的解决方案

方案一:用AVAssetExportSession(最简单的原生方法)

虽然AVAssetWriter不行,但AVAssetExportSession在macOS 10.15上是支持MP3导出的,这是最省事的原生方案。示例代码如下:

import AVFoundation

func exportToMP3(from inputURL: URL, to outputURL: URL) {
    let asset = AVAsset(url: inputURL)
    
    // 创建导出会话,预设可根据需求选择,比如用AVAssetExportPresetPassthrough如果源是MP3,或者用通用预设
    guard let exportSession = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetAppleM4A) else {
        print("无法创建导出会话")
        return
    }
    
    exportSession.outputURL = outputURL
    exportSession.outputFileType = .mp3
    
    // 开始异步导出
    exportSession.exportAsynchronously {
        DispatchQueue.main.async {
            switch exportSession.status {
            case .completed:
                print("MP3导出成功,路径:\(outputURL.path)")
            case .failed:
                print("导出失败:\(exportSession.error?.localizedDescription ?? "未知错误")")
            case .cancelled:
                print("导出被取消")
            default:
                break
            }
        }
    }
}

注意:如果你的源音频是其他格式,确保AVAssetExportSession能正确编码成MP3,部分情况下可能需要先检查音频轨道的兼容性。

方案二:用AudioToolbox手动编码(自定义控制)

如果需要更精细的编码控制(比如调整比特率、采样率),可以用AudioToolbox框架的AudioConverter来手动将PCM音频数据编码成MP3。这个方法相对复杂,但灵活性更高:

import AudioToolbox

func encodePCMToMP3(pcmData: Data, inputASBD: AudioStreamBasicDescription, outputURL: URL) {
    // 定义MP3输出格式
    var outputASBD = AudioStreamBasicDescription()
    outputASBD.mFormatID = kAudioFormatMPEGLayer3
    outputASBD.mSampleRate = inputASBD.mSampleRate
    outputASBD.mChannelsPerFrame = inputASBD.mChannelsPerFrame
    outputASBD.mBitsPerChannel = 0 // MP3不需要设置这个
    outputASBD.mBytesPerPacket = 0
    outputASBD.mFramesPerPacket = 1152 // MP3标准帧大小
    outputASBD.mBytesPerFrame = 0
    outputASBD.mFormatFlags = 0
    
    // 创建音频转换器
    var converter: AudioConverterRef?
    let converterStatus = AudioConverterNew(&inputASBD, &outputASBD, &converter)
    guard converterStatus == noErr, let audioConverter = converter else {
        print("创建音频转换器失败")
        return
    }
    
    // 创建输出MP3文件
    let fileManager = FileManager.default
    if fileManager.fileExists(atPath: outputURL.path) {
        try? fileManager.removeItem(at: outputURL)
    }
    var outputFile: AudioFileID?
    let fileStatus = AudioFileCreateWithURL(outputURL as CFURL, kAudioFileMP3Type, &outputASBD, .eraseFile, &outputFile)
    guard fileStatus == noErr, let mp3File = outputFile else {
        print("创建MP3文件失败")
        return
    }
    
    // 准备PCM数据缓冲
    var pcmBuffer = pcmData
    var packetIndex = 0
    let packetsPerBuffer = 1024
    
    // 定义输入数据回调
    let inputProc: AudioConverterComplexInputDataProc = { _, ioNumberDataPackets, ioData, _, userData in
        guard let data = userData?.assumingMemoryBound(to: Data.self).pointee else {
            return -1
        }
        let bytesPerPacket = Int(inputASBD.mBytesPerPacket)
        let totalPackets = data.count / bytesPerPacket
        let packetsToProvide = min(Int(ioNumberDataPackets.pointee), totalPackets - packetIndex)
        
        if packetsToProvide > 0 {
            ioData?.pointee.mNumberBuffers = 1
            ioData?.pointee.mBuffers.mData = UnsafeMutableRawPointer(mutating: data.advanced(by: packetIndex * bytesPerPacket).baseAddress)
            ioData?.pointee.mBuffers.mDataByteSize = UInt32(packetsToProvide * bytesPerPacket)
            ioData?.pointee.mBuffers.mNumberChannels = inputASBD.mChannelsPerFrame
            ioNumberDataPackets.pointee = UInt32(packetsToProvide)
            packetIndex += packetsToProvide
        } else {
            ioNumberDataPackets.pointee = 0
        }
        return noErr
    }
    
    // 开始转换并写入文件
    var outputBuffer = AudioBufferList()
    var packetsWritten = 0
    while true {
        var packets = packetsPerBuffer
        let status = AudioConverterFillComplexBuffer(audioConverter, inputProc, &pcmBuffer, &packets, &outputBuffer, nil)
        guard status == noErr, packets > 0 else {
            break
        }
        
        let writeStatus = AudioFileWritePackets(mp3File, false, outputBuffer.mBuffers.mDataByteSize, nil, UInt64(packetsWritten), &packets, outputBuffer.mBuffers.mData)
        if writeStatus != noErr {
            print("写入MP3文件失败")
            break
        }
        packetsWritten += packets
    }
    
    // 清理资源
    AudioFileClose(mp3File)
    AudioConverterDispose(audioConverter)
    print("MP3编码完成")
}

这个方法需要你先获取到源音频的PCM数据和对应的AudioStreamBasicDescription,适合需要自定义编码参数的场景。

方案三:集成第三方LAME库(更高灵活性)

如果原生方法满足不了你的需求,还可以集成LAME这个成熟的MP3编码库。你可以通过CocoaPods引入(比如pod 'lame'),或者手动编译集成,然后调用它的API将PCM数据编码成MP3。LAME支持更多自定义参数,比如VBR(可变比特率)编码等。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.09 19:47:45