当前位置: 首页 > article >正文

iOS 音频录制、播放与格式转换

iOS 音频录制、播放与格式转换:基于 AVFoundation 和 FFmpegKit 的实现

在 iOS 开发中,音频处理是一个非常常见的需求,比如录音、播放音频、音频格式转换等。本文将详细解读一段基于 AVFoundationFFmpegKit 的代码,展示如何实现音频录制、播放以及 PCM 和 AAC 格式之间的转换。

页面功能

在这里插入图片描述

功能概述

AVAudioRecorderManager.swift

import AVFoundation
import Foundation

// 定义协议,用于通知录音状态的变化
protocol AudioRecorderDelegate: AnyObject {
    func customAudioRecorderDidFinishRecording(successfully flag: Bool)
    func customAudioRecorderDidEncounterError(_ error: Error)
}

class AudioRecorder: NSObject {
    private var audioRecorder: AVAudioRecorder?
    private var recordingSession: AVAudioSession!
    weak var delegate: AudioRecorderDelegate?
    
    private var isRecording: Bool = false
    
    // 录音文件保存路径(可自定义)
    private var recordingFileURL: URL {
        // 获取 Documents 目录
        let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
        // 创建文件名,修改扩展名为 .wav
        let audioFilename = documentsPath.appendingPathComponent("recording.wav")
        return audioFilename
    }
    
    func setupAudioSessionPlayAndRecord() {
        do {
            // 设置音频会话类别和模式
            try recordingSession.setCategory(.playAndRecord, mode: .default)
            try recordingSession.setActive(true)
            print("音频会话已成功配置为录音模式")
        } catch {
            print("音频会话配置失败:\(error.localizedDescription)")
            delegate?.customAudioRecorderDidEncounterError(error)
        }
    }
    
    func setupAudioSessionPlayback() {
        do {
            // 设置音频会话类别和模式
            try recordingSession.setCategory(.playback, mode: .default)
            try recordingSession.setActive(true)
            print("音频会话已成功配置为播放模式")
        } catch {
            print("音频会话配置失败:\(error.localizedDescription)")
            delegate?.customAudioRecorderDidEncounterError(error)
        }
    }
    
    // 请求麦克风权限并设置音频会话排
    func requestPermissionAndSetupSession(completion: @escaping (Bool) -> Void) {
        recordingSession = AVAudioSession.sharedInstance()
        
        // 请求麦克风权限
        AVAudioSession.sharedInstance().requestRecordPermission { [unowned self] allowed in
            DispatchQueue.main.async {
                if allowed {
                    self.setupAudioSessionPlayAndRecord()
                } else {
                    print("麦克风权限被拒绝")
                    completion(false)
                }
            }
        }
    }
    
    // 开始录音
    func startRecording() {
        // 设置录音参数
        let settings: [String: Any] = [
            AVFormatIDKey: Int(kAudioFormatLinearPCM), // 音频格式改为 PCM
            AVSampleRateKey: 44100, // 采样率
            AVNumberOfChannelsKey: 2, // 声道数
            AVLinearPCMBitDepthKey: 16, // 位深度(常用 16 位), 使用多少个二进制来存储一个采样点的样本值,位深度越高,表示振幅越精确
            AVLinearPCMIsBigEndianKey: false, // 是否大端字节序
            AVLinearPCMIsFloatKey: false, // 是否浮点型
            AVLinearPCMIsNonInterleaved: false, // 是否非交错
        ]
        
        do {
            audioRecorder = try AVAudioRecorder(url: recordingFileURL, settings: settings)
            audioRecorder?.delegate = self
            audioRecorder?.record()
            isRecording = true
            print("开始录音")
        } catch {
            print("录音器初始化失败:\(error.localizedDescription)")
            delegate?.customAudioRecorderDidEncounterError(error)
        }
    }
    
    // 停止录音
    func stopRecording() {
        audioRecorder?.stop()
        isRecording = false
        print("停止录音")
    }
    
    // 判断是否正在录音
    func isRecordingActive() -> Bool {
        return isRecording
    }
    
    // 获取录音文件的 URL
    func getRecordingFileURL() -> URL {
        return recordingFileURL
    }
}

// MARK: - AVAudioRecorderDelegate

extension AudioRecorder: AVAudioRecorderDelegate {
    // 录音完成后的回调
    func audioRecorderDidFinishRecording(_ recorder: AVAudioRecorder, successfully flag: Bool) {
        print("录音完成")
        delegate?.customAudioRecorderDidFinishRecording(successfully: flag)
    }
    
    // 录音发生错误的回调
    func audioRecorderEncodeErrorDidOccur(_ recorder: AVAudioRecorder, error: Error?) {
        if let error = error {
            print("录音发生错误:\(error.localizedDescription)")
            delegate?.customAudioRecorderDidEncounterError(error)
        }
    }
}

ViewController.swift

import AVFoundation
import AVKit
import ffmpegkit
import UIKit


class ViewController: UIViewController {
    let audioRecorder = AudioRecorder()
    var audioPlayer: AVAudioPlayer?

    override func viewDidLoad() {
        super.viewDidLoad()
        
        debugPrint(FFmpegKitConfig.getVersion())

        // 设置代理
        audioRecorder.delegate = self
        
        // 请求权限并设置会话
        audioRecorder.requestPermissionAndSetupSession { [weak self] granted in
            if granted {
                print("麦克风权限已获得,可以开始录音")
                // 可以启用录音按钮等
            } else {
                print("麦克风权限被拒绝,无法录音")
                // 提示用户去设置中开启权限
            }
        }
        

        // 创建一个开始录音的按钮
        let recordButton = UIButton(type: .system)
        recordButton.setTitle("开始录音", for: .normal)
        recordButton.frame = CGRect(x: 100, y: 150, width: 200, height: 50)
        recordButton.addTarget(self, action: #selector(startRecording), for: .touchUpInside)
        view.addSubview(recordButton)
        
        // 创建一个停止录音的按钮
        let stopButton = UIButton(type: .system)
        stopButton.setTitle("停止录音,自动保存为pcm", for: .normal)
        stopButton.frame = CGRect(x: 100, y: 200, width: 200, height: 50)
        stopButton.addTarget(self, action: #selector(stopRecording), for: .touchUpInside)
        view.addSubview(stopButton)
        
        // 播放音频
        let playButton = UIButton(type: .system)
        playButton.setTitle("播放pcm音频", for: .normal)
        playButton.frame = CGRect(x: 100, y: 250, width: 200, height: 50)
        playButton.addTarget(self, action: #selector(playAudio), for: .touchUpInside)
        view.addSubview(playButton)
        
        // 转acc编码按钮
        let convertButton = UIButton(type: .system)
        convertButton.setTitle("pcm转换为aac格式", for: .normal)
        convertButton.frame = CGRect(x: 100, y: 300, width: 200, height: 50)
        convertButton.addTarget(self, action: #selector(convertToAAC), for: .touchUpInside)
        view.addSubview(convertButton)
        
        // acc 转 pcm 按钮
        let convertToPCMButton = UIButton(type: .system)
        convertToPCMButton.setTitle("acc转换为pcm格式", for: .normal)
        convertToPCMButton.frame = CGRect(x: 100, y: 350, width: 200, height: 50)
        convertToPCMButton.addTarget(self, action: #selector(accConvertToPCM), for: .touchUpInside)
        view.addSubview(convertToPCMButton)
    }

    // MARK: - Actions
    
    // 开始录音按钮的操作
    @objc func startRecording() {
        if !audioRecorder.isRecordingActive() {
            audioRecorder.startRecording()
        } else {
            print("正在录音中")
        }
    }
        
    // 停止录音按钮的操作
    @objc func stopRecording() {
        if audioRecorder.isRecordingActive() {
            audioRecorder.stopRecording()
        } else {
            print("当前未在录音")
        }
    }
    
    @objc func playAudio() {
        let audioURL = audioRecorder.getRecordingFileURL()
        print("播放音频文件路径:\(audioURL.path)")
        
        // 必须要设置一下AVAudioSession模式,否则播放不出来
        audioRecorder.setupAudioSessionPlayback()
        
        
        if FileManager.default.fileExists(atPath: audioURL.path) {
            do {
                audioPlayer = try AVAudioPlayer(contentsOf: audioURL)
                audioPlayer?.delegate = self
                audioPlayer?.play()
                
                print("音频播放中:\(audioPlayer?.isPlaying ?? false)")
                print("音频时长:\(audioPlayer?.duration ?? 0) 秒")
            } catch {
                print("音频播放失败:\(error.localizedDescription)")
            }
        } else {
            print("音频文件不存在")
        }
    }
    
    // MARK: - AudioRecorderDelegate

    func audioRecorderDidFinishRecording(successfully flag: Bool) {
        if flag {
            print("录音成功,文件保存于:\(audioRecorder.getRecordingFileURL().path)")
            // 在此可以对录音文件进行操作,例如播放或上传
        } else {
            print("录音失败")
        }
    }
        
    func audioRecorderDidEncounterError(_ error: Error) {
        print("录音发生错误:\(error.localizedDescription)")
        // 在此进行错误处理
    }
    
    // ACC 转 PCM ( 解码 )
    @objc func accConvertToPCM() {
        // 显示进度条
        SVProgressHUD.show()
        
        // 设置输出文件路径
        let documentsDir = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
        let inputFile = "\(documentsDir)/recording.aac"
        
        // 设置输出文件路径
        let outputFile = "\(documentsDir)/acc_to_recording.pcm"
    
        // 是否覆盖
        let shouldOverwrite = true // 或者根据需要设置为 false
        let overwriteOption = shouldOverwrite ? "-y" : ""
        let ffmpegCommand = "\(overwriteOption) -c:a aac  -i \"\(inputFile)\" -f s16le \"\(outputFile)\""
        print("执行的命令:ffmpeg \(ffmpegCommand)")
        // 执行转换
        FFmpegKit.executeAsync(ffmpegCommand) { session in
            
            let returnCode = session?.getReturnCode()
            
            print("Return Code:\(String(describing: returnCode))")
            
            if ReturnCode.isSuccess(returnCode) {
                // 转换成功
                print("音频转换成功!输出文件位于:\(outputFile)")
                DispatchQueue.main.async {
                    SVProgressHUD.dismiss()
                    // 弹窗提示成功,点击确定播放
                    let alert = UIAlertController(title: "转换成功", message: "输出文件位于:\(outputFile)", preferredStyle: .alert)
                    
                    // 确定按钮
                    let okAction = UIAlertAction(title: "播放", style: .default) { _ in
                        // 播放输出文件
                        let player = AVPlayer(url: URL(fileURLWithPath: outputFile))
                        let playerViewController = AVPlayerViewController()
                        playerViewController.player = player
                        self.present(playerViewController, animated: true) {
                            player.play()
                        }
                    }
                    
                    // alert show
                    alert.addAction(okAction)
                    
                    // show
                    self.present(alert, animated: true, completion: nil)
                }
                
            } else {
                // 转换失败
                if let output = session?.getAllLogsAsString() {
                    print("转换失败,输出日志:\n\(output)")
                }
            }
        }
    }
    
    // pcm 转 acc (编码)
    @objc func convertToAAC() {
        // 显示进度条
        SVProgressHUD.show()
        
        let inputFile = audioRecorder.getRecordingFileURL().path
        
        // 设置输出文件路径
        let documentsDir = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
        let outputFile = "\(documentsDir)/recording.aac"
    
        // 是否覆盖
        let shouldOverwrite = true // 或者根据需要设置为 false
        let overwriteOption = shouldOverwrite ? "-y" : ""
        let ffmpegCommand = "\(overwriteOption) -ar 44100 -ac 2 -f s16le -i \"\(inputFile)\" -c:a aac \"\(outputFile)\""
        print("执行的命令:ffmpeg \(ffmpegCommand)")
        // 执行转换
        FFmpegKit.executeAsync(ffmpegCommand) { session in
            
            let returnCode = session?.getReturnCode()
            
            print("Return Code:\(String(describing: returnCode))")
            
            if ReturnCode.isSuccess(returnCode) {
                // 转换成功
                print("音频转换成功!输出文件位于:\(outputFile)")
                DispatchQueue.main.async {
                    SVProgressHUD.dismiss()
                    // 弹窗提示成功,点击确定播放
                    let alert = UIAlertController(title: "转换成功", message: "输出文件位于:\(outputFile)", preferredStyle: .alert)
                    
                    // 确定按钮
                    let okAction = UIAlertAction(title: "播放", style: .default) { _ in
                        // 播放输出文件
                        let player = AVPlayer(url: URL(fileURLWithPath: outputFile))
                        let playerViewController = AVPlayerViewController()
                        playerViewController.player = player
                        self.present(playerViewController, animated: true) {
                            player.play()
                        }
                    }
                    
                    // alert show
                    alert.addAction(okAction)
                    
                    // show
                    self.present(alert, animated: true, completion: nil)
                }
                
            } else {
                // 转换失败
                if let output = session?.getAllLogsAsString() {
                    print("转换失败,输出日志:\n\(output)")
                }
            }
        }
    }
}


// MARK: - AudioRecorderDelegate
extension ViewController: AudioRecorderDelegate {
    func customAudioRecorderDidFinishRecording(successfully flag: Bool) {
        if flag {
            print("录音成功,文件保存于:\(audioRecorder.getRecordingFileURL().path)")
            // 在此可以对录音文件进行操作,例如播放或上传
        } else {
            print("录音失败")
        }
    }
    
    func customAudioRecorderDidEncounterError(_ error: Error) {
        print("录音发生错误:\(error.localizedDescription)")
        // 在此进行错误处理
    }
}

// MARK: - AVAudioPlayerDelegate
extension ViewController: AVAudioPlayerDelegate {
    func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
        print("音频播放完成")
    }
    
    func audioPlayerDecodeErrorDidOccur(_ player: AVAudioPlayer, error: Error?) {
        if let error = error {
            print("音频解码发生错误:\(error.localizedDescription)")
        }
    }
}

这段代码实现了以下功能:

  1. 音频录制:通过 AVAudioRecorder 录制音频,并保存为 PCM 格式的 .wav 文件。
  2. 音频播放:通过 AVAudioPlayer 播放录制的音频。
  3. 音频格式转换
    • 使用 FFmpegKit 将 PCM 格式的音频转换为 AAC 格式。
    • 使用 FFmpegKit 将 AAC 格式的音频解码为 PCM 格式。
  4. 用户交互:通过按钮触发录音、停止录音、播放音频以及格式转换操作。

接下来,我们将逐步解析代码的实现细节。


1. 项目依赖

1.1 AVFoundation 框架

AVFoundation 是 iOS 提供的音频和视频处理框架,支持录音、播放、音频会话管理等功能。本文的录音和播放功能均基于 AVFoundation 实现。

1.2 FFmpegKit

FFmpegKit 是 FFmpeg 的封装库,支持音视频的编码、解码、格式转换等操作。本文使用 FFmpegKit 实现音频格式的转换。


2. 代码结构

代码主要分为以下几个部分:

  1. AudioRecorder:封装了录音相关的逻辑,包括音频会话配置、录音启动与停止、录音文件管理等。
  2. ViewController:负责用户交互和界面展示,调用 AudioRecorder 进行录音,并使用 FFmpegKit 进行音频格式转换。
  3. 音频格式转换逻辑:通过 FFmpegKit 执行 PCM 和 AAC 格式之间的转换。

3. 代码解析

3.1 AudioRecorder

AudioRecorder 是一个封装类,负责管理录音的相关逻辑。以下是其主要功能:

3.1.1 音频会话配置

在 iOS 中,音频会话 (AVAudioSession) 决定了应用的音频行为。AudioRecorder 提供了两种会话配置:

  • 录音模式:用于录音时的音频会话配置。
  • 播放模式:用于播放音频时的音频会话配置。
func setupAudioSessionPlayAndRecord() {
    do {
        try recordingSession.setCategory(.playAndRecord, mode: .default)
        try recordingSession.setActive(true)
        print("音频会话已成功配置为录音模式")
    } catch {
        print("音频会话配置失败:\(error.localizedDescription)")
        delegate?.customAudioRecorderDidEncounterError(error)
    }
}

func setupAudioSessionPlayback() {
    do {
        try recordingSession.setCategory(.playback, mode: .default)
        try recordingSession.setActive(true)
        print("音频会话已成功配置为播放模式")
    } catch {
        print("音频会话配置失败:\(error.localizedDescription)")
        delegate?.customAudioRecorderDidEncounterError(error)
    }
}
3.1.2 请求麦克风权限

在录音之前,需要请求用户的麦克风权限。如果用户拒绝权限,录音功能将无法使用。

func requestPermissionAndSetupSession(completion: @escaping (Bool) -> Void) {
    recordingSession = AVAudioSession.sharedInstance()
    
    AVAudioSession.sharedInstance().requestRecordPermission { [unowned self] allowed in
        DispatchQueue.main.async {
            if allowed {
                self.setupAudioSessionPlayAndRecord()
            } else {
                print("麦克风权限被拒绝")
                completion(false)
            }
        }
    }
}
3.1.3 录音逻辑

录音使用 AVAudioRecorder 实现,录音文件保存为 PCM 格式的 .wav 文件。

func startRecording() {
    let settings: [String: Any] = [
        AVFormatIDKey: Int(kAudioFormatLinearPCM),
        AVSampleRateKey: 44100,
        AVNumberOfChannelsKey: 2,
        AVLinearPCMBitDepthKey: 16,
        AVLinearPCMIsBigEndianKey: false,
        AVLinearPCMIsFloatKey: false,
        AVLinearPCMIsNonInterleaved: false,
    ]
    
    do {
        audioRecorder = try AVAudioRecorder(url: recordingFileURL, settings: settings)
        audioRecorder?.delegate = self
        audioRecorder?.record()
        isRecording = true
        print("开始录音")
    } catch {
        print("录音器初始化失败:\(error.localizedDescription)")
        delegate?.customAudioRecorderDidEncounterError(error)
    }
}

3.2 ViewController

ViewController 是主界面控制器,负责用户交互和界面展示。以下是其主要功能:

3.2.1 用户界面

通过按钮触发录音、停止录音、播放音频以及格式转换操作。

let recordButton = UIButton(type: .system)
recordButton.setTitle("开始录音", for: .normal)
recordButton.frame = CGRect(x: 100, y: 150, width: 200, height: 50)
recordButton.addTarget(self, action: #selector(startRecording), for: .touchUpInside)
view.addSubview(recordButton)
3.2.2 音频播放

音频播放使用 AVAudioPlayer 实现。

@objc func playAudio() {
    let audioURL = audioRecorder.getRecordingFileURL()
    audioRecorder.setupAudioSessionPlayback()
    
    if FileManager.default.fileExists(atPath: audioURL.path) {
        do {
            audioPlayer = try AVAudioPlayer(contentsOf: audioURL)
            audioPlayer?.delegate = self
            audioPlayer?.play()
        } catch {
            print("音频播放失败:\(error.localizedDescription)")
        }
    } else {
        print("音频文件不存在")
    }
}

3.3 音频格式转换

音频格式转换使用 FFmpegKit 实现。以下是 PCM 和 AAC 格式之间的转换逻辑。

3.3.1 PCM 转 AAC

通过 FFmpegKit 将 PCM 格式的 .wav 文件编码为 AAC 格式。

@objc func convertToAAC() {
    let inputFile = audioRecorder.getRecordingFileURL().path
    let documentsDir = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
    let outputFile = "\(documentsDir)/recording.aac"
    
    let ffmpegCommand = "-y -ar 44100 -ac 2 -f s16le -i \"\(inputFile)\" -c:a aac \"\(outputFile)\""
    FFmpegKit.executeAsync(ffmpegCommand) { session in
        let returnCode = session?.getReturnCode()
        if ReturnCode.isSuccess(returnCode) {
            print("音频转换成功!输出文件位于:\(outputFile)")
        } else {
            print("音频转换失败")
        }
    }
}
3.3.2 AAC 转 PCM

通过 FFmpegKit 将 AAC 格式的文件解码为 PCM 格式。

@objc func accConvertToPCM() {
    let documentsDir = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
    let inputFile = "\(documentsDir)/recording.aac"
    let outputFile = "\(documentsDir)/acc_to_recording.pcm"
    
    let ffmpegCommand = "-y -c:a aac -i \"\(inputFile)\" -f s16le \"\(outputFile)\""
    FFmpegKit.executeAsync(ffmpegCommand) { session in
        let returnCode = session?.getReturnCode()
        if ReturnCode.isSuccess(returnCode) {
            print("音频转换成功!输出文件位于:\(outputFile)")
        } else {
            print("音频转换失败")
        }
    }
}

4. 总结

本文通过 AVFoundationFFmpegKit 实现了音频录制、播放以及格式转换功能。以下是实现的关键点:

  1. 音频会话管理:通过 AVAudioSession 配置录音和播放模式。
  2. 录音与播放:使用 AVAudioRecorderAVAudioPlayer 实现。
  3. 格式转换:使用 FFmpegKit 执行 PCM 和 AAC 格式之间的转换。

这段代码展示了如何在 iOS 中处理音频的完整流程,适合需要音频处理功能的开发者参考和使用。


http://www.kler.cn/a/534908.html

相关文章:

  • Windows本地部署DeepSeek-R1大模型并使用web界面远程交互
  • 专门记录台式电脑常见问题
  • 二、CSS笔记
  • SpringBoot+Dubbo+zookeeper 急速入门案例
  • JavaScript前后端交互-AJAX/fetch
  • 64位的谷歌浏览器Chrome/Google Chrome
  • Linux常见问题解决方法--2
  • k8s中,一.pod污点,二.pod容器污点容忍策略,三.pod优先级(PriorityClass类)
  • 深度学习 | 表示学习 | 卷积神经网络 | Batch Normalization 在 CNN 中的示例 | 20
  • RFID隧道机:提升生产流水线效率与精准度
  • 【Java报错解决】警告: 源发行版 11 需要目标发行版 11
  • 教育系统软件正版化:信创替换的加速引擎
  • Linux里的容器被OOM killed的两种情况
  • 100.8 AI量化面试题:如何使用自监督学习方法从原始市场数据中挖掘新的alpha因子?
  • 我用Ai学Android Jetpack Compose之CircularProgressIndicator
  • MongoDB学习笔记-解析jsonCommand内容
  • Unix/Linux编程:fcntl函数总结
  • vscode 如何通过Continue引入AI 助手deepseek
  • 国产编辑器EverEdit - 自定义标记使用详解
  • python爬虫--简单登录
  • 无界构建微前端?NO!NO!NO!多系统融合思路!
  • HTML 复习
  • [SAP ABAP] 面向对象程序设计-类的访问区域
  • 【React】合成事件语法
  • 防静电监控看板如何助力生产线提升品质管理效率
  • C语言基础系列【4】C语言基础语法