iOS 音频录制、播放与格式转换
iOS 音频录制、播放与格式转换:基于 AVFoundation 和 FFmpegKit 的实现
在 iOS 开发中,音频处理是一个非常常见的需求,比如录音、播放音频、音频格式转换等。本文将详细解读一段基于 AVFoundation
和 FFmpegKit
的代码,展示如何实现音频录制、播放以及 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)")
}
}
}
这段代码实现了以下功能:
- 音频录制:通过
AVAudioRecorder
录制音频,并保存为 PCM 格式的.wav
文件。 - 音频播放:通过
AVAudioPlayer
播放录制的音频。 - 音频格式转换:
- 使用
FFmpegKit
将 PCM 格式的音频转换为 AAC 格式。 - 使用
FFmpegKit
将 AAC 格式的音频解码为 PCM 格式。
- 使用
- 用户交互:通过按钮触发录音、停止录音、播放音频以及格式转换操作。
接下来,我们将逐步解析代码的实现细节。
1. 项目依赖
1.1 AVFoundation 框架
AVFoundation
是 iOS 提供的音频和视频处理框架,支持录音、播放、音频会话管理等功能。本文的录音和播放功能均基于 AVFoundation
实现。
1.2 FFmpegKit
FFmpegKit
是 FFmpeg 的封装库,支持音视频的编码、解码、格式转换等操作。本文使用 FFmpegKit
实现音频格式的转换。
2. 代码结构
代码主要分为以下几个部分:
AudioRecorder
类:封装了录音相关的逻辑,包括音频会话配置、录音启动与停止、录音文件管理等。ViewController
类:负责用户交互和界面展示,调用AudioRecorder
进行录音,并使用FFmpegKit
进行音频格式转换。- 音频格式转换逻辑:通过
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. 总结
本文通过 AVFoundation
和 FFmpegKit
实现了音频录制、播放以及格式转换功能。以下是实现的关键点:
- 音频会话管理:通过
AVAudioSession
配置录音和播放模式。 - 录音与播放:使用
AVAudioRecorder
和AVAudioPlayer
实现。 - 格式转换:使用
FFmpegKit
执行 PCM 和 AAC 格式之间的转换。
这段代码展示了如何在 iOS 中处理音频的完整流程,适合需要音频处理功能的开发者参考和使用。