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

使用Audio Queue Services录制M4A音频失败:文件无法打开或崩溃

解决Audio Queue Services录制M4A(AAC编码)文件的问题

核心问题根源

Audio Queue从麦克风获取的是原始PCM数据,无法直接录制AAC压缩格式;同时你在创建文件时混淆了「编码格式ID」和「文件容器类型ID」,这是导致文件无法打开或崩溃的主要原因。AVAudioRecorder内部帮你封装了PCM转AAC的编码流程,但Audio Queue需要手动实现这一步骤。

关键修正点

  1. 文件类型ID错误:AudioFileCreateWithURL的第二个参数必须是kAudioFileM4AType(M4A文件容器),而非kAudioFormatMPEG4AAC(编码格式ID)。
  2. 缺少编码步骤:必须通过AudioConverterRef将录制的PCM数据编码为AAC格式,再写入M4A文件。
  3. AAC格式配置不完整:AAC的AudioStreamBasicDescription需通过系统API获取默认配置,不能手动随意设置字段。

完整实现代码

以下是可直接运行的Swift示例,包含PCM录制、AAC编码、M4A文件写入的完整流程:

import AVFoundation

class AACAudioRecorder {
    private var audioQueue: AudioQueueRef?
    private var outputAudioFile: AudioFileID?
    private var audioConverter: AudioConverterRef?
    
    // 录制的PCM格式(与你之前的LinearPCM配置一致)
    private let recordingPCMFormat = AudioStreamBasicDescription(
        mFormatID: kAudioFormatLinearPCM,
        mSampleRate: 16000,
        mChannelsPerFrame: 1,
        mBitsPerChannel: 16,
        mBytesPerPacket: 2,
        mBytesPerFrame: 2,
        mFramesPerPacket: 1,
        mFormatFlags: kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked
    )
    private var targetAACFormat = AudioStreamBasicDescription()
    
    init() {
        setupAACFormat()
        setupOutputAudioFile()
        setupAudioConverter()
        setupRecordingQueue()
    }
    
    // 获取AAC编码器的默认配置
    private func setupAACFormat() {
        var propSize = UInt32(MemoryLayout<AudioStreamBasicDescription>.size)
        var formatID = kAudioFormatMPEG4AAC
        
        CheckError(
            AudioFormatGetProperty(
                kAudioFormatProperty_FormatInfo,
                0,
                &formatID,
                &propSize,
                &targetAACFormat
            ),
            "获取AAC格式配置失败"
        )
        
        // 匹配录制的采样率和通道数
        targetAACFormat.mSampleRate = recordingPCMFormat.mSampleRate
        targetAACFormat.mChannelsPerFrame = recordingPCMFormat.mChannelsPerFrame
    }
    
    // 创建M4A输出文件
    private func setupOutputAudioFile() {
        let documentsDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
        let outputURL = documentsDir.appendingPathComponent("recording.m4a")
        
        CheckError(
            AudioFileCreateWithURL(
                outputURL as CFURL,
                kAudioFileM4AType,
                &targetAACFormat,
                .eraseFile,
                &outputAudioFile
            ),
            "创建M4A文件失败"
        )
    }
    
    // 创建PCM转AAC的编码器
    private func setupAudioConverter() {
        CheckError(
            AudioConverterNew(
                &recordingPCMFormat,
                &targetAACFormat,
                &audioConverter
            ),
            "创建音频编码器失败"
        )
        
        // 设置编码质量(对应AVAudioQuality.high)
        var quality = Int32(AVAudioQuality.high.rawValue)
        let propSize = UInt32(MemoryLayout<Int32>.size)
        CheckError(
            AudioConverterSetProperty(
                audioConverter!,
                kAudioConverterCodecQuality,
                propSize,
                &quality
            ),
            "设置编码质量失败"
        )
    }
    
    // 初始化录制队列
    private func setupRecordingQueue() {
        let bufferSize = calculateOptimalBufferSize()
        
        CheckError(
            AudioQueueNewInput(
                &recordingPCMFormat,
                recordingCallback,
                Unmanaged.passUnretained(self).toOpaque(),
                nil,
                nil,
                0,
                &audioQueue
            ),
            "创建录制队列失败"
        )
        
        // 分配3个缓冲区循环使用
        for _ in 0..<3 {
            var buffer: AudioQueueBufferRef?
            CheckError(
                AudioQueueAllocateBuffer(audioQueue!, bufferSize, &buffer),
                "分配队列缓冲区失败"
            )
            CheckError(
                AudioQueueEnqueueBuffer(audioQueue!, buffer!, 0, nil),
                "加入队列缓冲区失败"
            )
        }
    }
    
    // 计算0.5秒PCM数据对应的缓冲区大小,避免卡顿
    private func calculateOptimalBufferSize() -> UInt32 {
        let seconds: Double = 0.5
        let bytesPerSecond = recordingPCMFormat.mSampleRate * Double(recordingPCMFormat.mBytesPerFrame)
        return UInt32(seconds * bytesPerSecond)
    }
    
    // 录制回调:PCM转AAC后写入文件
    private let recordingCallback: AudioQueueInputCallback = { userData, queue, buffer, _, packetCount, _ in
        let recorder = Unmanaged<AACAudioRecorder>.fromOpaque(userData!).takeUnretainedValue()
        
        guard packetCount > 0, let converter = recorder.audioConverter, let file = recorder.outputAudioFile else {
            AudioQueueEnqueueBuffer(queue, buffer!, 0, nil)
            return
        }
        
        // 封装PCM输入数据
        var inputData = AudioConverterInputData(
            data: buffer!.mAudioData,
            size: buffer!.mAudioDataByteSize,
            packetCount: packetCount
        )
        
        // 准备AAC输出缓冲区
        var outputPacketCount = UInt32(1)
        var outputBuffer = [UInt8](repeating: 0, count: 1024)
        
        // 执行PCM转AAC编码
        let status = AudioConverterFillComplexBuffer(
            converter,
            { _, outPacketCount, outData, _, inputDataPtr in
                let inputData = inputDataPtr!.load(as: AudioConverterInputData.self)
                outData?.mAudioData = inputData.data
                outData?.mAudioDataByteSize = inputData.size
                outPacketCount.pointee = inputData.packetCount
                return noErr
            },
            &inputData,
            &outputPacketCount,
            &outputBuffer,
            nil
        )
        
        // 将编码后的AAC数据写入文件
        if status == noErr && outputPacketCount > 0 {
            var bytesWritten = UInt32(0)
            AudioFileWriteBytes(file, false, 0, &bytesWritten, outputBuffer)
        }
        
        // 缓冲区重新入队继续录制
        AudioQueueEnqueueBuffer(queue, buffer!, 0, nil)
    }
    
    // 开始录制
    func start() {
        CheckError(AudioQueueStart(audioQueue!, nil), "启动录制失败")
    }
    
    // 停止录制并释放资源
    func stop() {
        AudioQueueStop(audioQueue!, true)
        AudioQueueDispose(audioQueue!, true)
        if let file = outputAudioFile {
            AudioFileClose(file)
        }
    }
}

// 辅助结构体:传递编码输入数据
fileprivate struct AudioConverterInputData {
    var data: UnsafeMutableRawPointer?
    var size: UInt32
    var packetCount: UInt32
}

// 错误检查工具函数
func CheckError(_ status: OSStatus, _ message: String) {
    guard status != noErr else { return }
    print("\(message):错误码 \(status)")
    fatalError()
}

使用说明

  1. 初始化AACAudioRecorder实例,调用start()开始录制,stop()停止录制。
  2. 录制的M4A文件会保存在应用的Documents目录下。
  3. 实际项目中可优化错误处理逻辑,避免直接fatalError。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.29 03:20:01