You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多
文档控制台
免费开始使用

iOS/Swift:如何通过AVAudioEngine在所有硬件上录制16kHz音频

实现全硬件兼容的16kHz单声道Int16音频录制(Swift + AVAudioEngine)

嘿,我来帮你搞定这个全硬件兼容的16kHz音频录制需求!你的基础代码方向是对的,但要覆盖所有iOS设备,得处理几个关键的兼容性细节——比如不同硬件原生采样率不一致、权限检查、格式转换可靠性这些问题。下面是优化后的完整实现方案:

核心优化点与完整代码

1. 先搞定麦克风权限

iOS对麦克风权限管得很严,必须先确保用户授权了才能录制,不然直接启动引擎会崩溃:

import AVFoundation

// 检查并请求麦克风权限
func checkMicrophonePermission(completion: @escaping (Bool) -> Void) {
    switch AVAudioSession.sharedInstance().recordPermission {
    case .granted:
        completion(true)
    case .denied:
        completion(false)
    case .undetermined:
        AVAudioSession.sharedInstance().requestRecordPermission { granted in
            completion(granted)
        }
    @unknown default:
        completion(false)
    }
}

2. 兼容所有硬件的录制实现

关键思路:不要强制要求硬件直接输出16kHz格式——不同iOS设备的麦克风原生采样率可能是44.1kHz、48kHz甚至其他值,硬指定会导致部分设备崩溃。我们用AVAudioConverter在回调里做可靠的软件转换,同时正确配置音频会话:

class AudioRecorder {
    private let audioEngine = AVAudioEngine()
    private let desiredFormat: AVAudioFormat
    private var converter: AVAudioConverter?
    
    init() {
        // 我们最终需要的格式:16kHz、单声道、Int16 PCM
        desiredFormat = AVAudioFormat(commonFormat: .pcmFormatInt16,
                                     sampleRate: 16000.0,
                                     channels: 1,
                                     interleaved: false)!
    }
    
    func startRecording() throws {
        // 配置音频会话为录制模式,避免和其他音频应用冲突
        let session = AVAudioSession.sharedInstance()
        try session.setCategory(.record, mode: .default, options: [])
        try session.setActive(true)
        
        let inputNode = audioEngine.inputNode
        let hardwareInputFormat = inputNode.inputFormat(forBus: 0)
        
        // 初始化格式转换器:把硬件原生格式转成我们要的16kHz格式
        converter = AVAudioConverter(from: hardwareInputFormat, to: desiredFormat)
        
        // 直接给输入节点加tap,不需要额外的mixer(除非你要混音其他音频)
        inputNode.installTap(onBus: 0, bufferSize: 2048, format: hardwareInputFormat) { [weak self] buffer, time in
            guard let self = self, let converter = self.converter else { return }
            
            // 准备对应目标格式的buffer
            guard let convertedBuffer = AVAudioPCMBuffer(pcmFormat: self.desiredFormat,
                                                         frameCapacity: AVAudioFrameCount(buffer.frameLength * self.desiredFormat.sampleRate / hardwareInputFormat.sampleRate)) else {
                return
            }
            
            // 执行格式转换
            var error: NSError?
            converter.convert(to: convertedBuffer, error: &error) { inNumPackets, outStatus in
                outStatus.pointee = .haveData
                return buffer
            }
            
            if let error = error {
                print("格式转换出错:\(error.localizedDescription)")
                return
            }
            
            // 这里拿到的convertedBuffer就是符合要求的16kHz、Int16、单声道音频数据
            self.processAudioData(from: convertedBuffer)
        }
        
        // 启动音频引擎
        try audioEngine.start()
    }
    
    func stopRecording() {
        audioEngine.stop()
        audioEngine.inputNode.removeTap(onBus: 0)
        do {
            try AVAudioSession.sharedInstance().setActive(false)
        } catch {
            print("停止音频会话失败:\(error.localizedDescription)")
        }
    }
    
    private func processAudioData(from buffer: AVAudioPCMBuffer) {
        // 示例:把Int16格式的PCM数据转成Data,方便后续处理(比如上传、保存)
        guard let channelData = buffer.int16ChannelData?[0] else { return }
        let frameCount = Int(buffer.frameLength)
        let audioData = Data(bytes: channelData, count: frameCount * MemoryLayout<Int16>.stride)
        
        // 这里可以根据需求处理音频数据,比如打印日志、发送到服务器等
        print("收到\(audioData.count)字节的16kHz音频数据")
    }
}

全硬件兼容的关键细节

  • 适配硬件原生格式:不同设备麦克风支持的原生采样率不一样,直接要求硬件输出16kHz会触发崩溃,用AVAudioConverter做软件转换是最稳妥的方式。
  • 音频会话配置:必须设置.record类别,并且启动/停止录制时要对应激活/停用会话,避免和其他音频应用抢资源。
  • 错误处理:所有涉及try的操作都要捕获错误,包括音频引擎启动、会话配置等,避免意外崩溃。
  • 避免循环引用:在tap回调里用[weak self],防止AudioRecorder实例无法被释放。

使用示例

let audioRecorder = AudioRecorder()
checkMicrophonePermission { granted in
    if granted {
        do {
            try audioRecorder.startRecording()
            // 模拟录制5秒后停止
            DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
                audioRecorder.stopRecording()
            }
        } catch {
            print("启动录制失败:\(error.localizedDescription)")
        }
    } else {
        print("麦克风权限未授权,请前往设置开启权限")
    }
}

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

火山引擎 最新活动