iOS Swift中如何用AVAudioRecorder与AVAudioPlayer实现实时音频录放?
Hey there! Let's walk through building this real-time megaphone app step by step, using the APIs you specified, plus share some critical tips to avoid common pitfalls.
整体思路
Your goal is to record audio to a temporary file and play it back instantly without processing. The core flow is:
- Request microphone permission (mandatory for iOS)
- Create a temporary audio file to store recordings
- Configure audio session to support both recording and playback
- Use a single button to toggle between "start (record + play)" and "stop (record + play)" states
- Clean up temporary files when done
核心步骤实现
1. 导入框架与声明实例
First, import AVFoundation and declare your core objects in your view controller:
import UIKit import AVFoundation class MegaphoneViewController: UIViewController { private var audioRecorder: AVAudioRecorder? private var audioPlayer: AVAudioPlayer? private var isRecording = false private var tempAudioPath: String? // 你的按钮IBOutlet @IBOutlet weak var toggleButton: UIButton! }
2. 请求麦克风权限
Add this to viewDidLoad() to request permission before any audio operations:
override func viewDidLoad() { super.viewDidLoad() requestMicrophonePermission() } private func requestMicrophonePermission() { AVAudioSession.sharedInstance().requestRecordPermission { granted in DispatchQueue.main.async { if !granted { // 提示用户开启麦克风权限 let alert = UIAlertController(title: "权限需求", message: "需要麦克风权限才能使用扩音器功能,请在设置中开启", preferredStyle: .alert) alert.addAction(UIAlertAction(title: "确定", style: .default)) self.present(alert, animated: true) } } } }
Don't forget to add NSMicrophoneUsageDescription to your Info.plist with a clear explanation of why you need the microphone.
3. 创建临时音频文件
Generate a unique temporary file path to avoid overwriting existing files:
private func createTempAudioFile() -> String? { let tempDir = NSTemporaryDirectory() let fileName = "temp_megaphone_\(UUID().uuidString).m4a" let filePath = tempDir.appending(fileName) return filePath }
4. 配置音频会话
Set up the audio session to support both recording and playback, and route audio to the speaker (not the earpiece):
private func configureAudioSession() { let session = AVAudioSession.sharedInstance() do { try session.setCategory(.playAndRecord, mode: .default, options: [.defaultToSpeaker, .allowBluetooth]) try session.setActive(true) } catch { print("音频会话配置失败: \(error.localizedDescription)") } }
5. 按钮启停逻辑
Implement the toggle action for your button:
@IBAction func toggleMegaphone(_ sender: UIButton) { isRecording.toggle() if isRecording { // 开始录制和播放 toggleButton.setTitle("停止", for: .normal) configureAudioSession() startRecordingAndPlayback() } else { // 停止录制和播放 toggleButton.setTitle("开始", for: .normal) stopRecordingAndPlayback() } } private func startRecordingAndPlayback() { guard let filePath = createTempAudioFile() else { print("创建临时文件失败") return } tempAudioPath = filePath // 配置AVAudioRecorder let settings: [String: Any] = [ AVFormatIDKey: Int(kAudioFormatMPEG4AAC), AVSampleRateKey: 44100.0, AVNumberOfChannelsKey: 1, AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue ] do { audioRecorder = try AVAudioRecorder(url: URL(fileURLWithPath: filePath), settings: settings) audioRecorder?.delegate = self audioRecorder?.record() // 延迟启动播放(因为文件需要先写入部分数据) DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { self.startPlayback() } } catch { print("录制启动失败: \(error.localizedDescription)") isRecording = false toggleButton.setTitle("开始", for: .normal) } } private func startPlayback() { guard let filePath = tempAudioPath else { return } do { audioPlayer = try AVAudioPlayer(contentsOf: URL(fileURLWithPath: filePath)) audioPlayer?.numberOfLoops = -1 // 循环播放直到停止 audioPlayer?.play() } catch { print("播放启动失败: \(error.localizedDescription)") } } private func stopRecordingAndPlayback() { audioRecorder?.stop() audioPlayer?.stop() // 清理临时文件 if let filePath = tempAudioPath { try? FileManager.default.removeItem(atPath: filePath) tempAudioPath = nil } audioRecorder = nil audioPlayer = nil // 停用音频会话 do { try AVAudioSession.sharedInstance().setActive(false) } catch { print("停用音频会话失败: \(error.localizedDescription)") } }
6. 实现AVAudioRecorderDelegate(可选)
Add delegate methods to handle recording completion or errors:
extension MegaphoneViewController: AVAudioRecorderDelegate { func audioRecorderDidFinishRecording(_ recorder: AVAudioRecorder, successfully flag: Bool) { if !flag { print("录制完成但失败") stopRecordingAndPlayback() isRecording = false toggleButton.setTitle("开始", for: .normal) } } }
关键注意事项与建议
1. 关于实时延迟的问题
Using AVAudioRecorder + AVAudioPlayer will introduce noticeable latency (hundreds of milliseconds) because you're recording to a file first then playing it back. For true real-time megaphone functionality, I strongly recommend using AVAudioEngine instead. It lets you route the microphone input directly to the speaker output with near-zero latency. Here's a quick snippet for that:
private var audioEngine: AVAudioEngine? private func startRealTimeMegaphone() { let engine = AVAudioEngine() let inputNode = engine.inputNode let outputNode = engine.outputNode let inputFormat = inputNode.inputFormat(forBus: 0) engine.connect(inputNode, to: outputNode, format: inputFormat) do { try engine.start() audioEngine = engine } catch { print("音频引擎启动失败: \(error.localizedDescription)") } } private func stopRealTimeMegaphone() { audioEngine?.stop() audioEngine = nil }
2. 音频会话细节
- Always handle audio session interruptions (like incoming calls) by pausing/stopping recording/playback, then resuming when the interruption ends.
- If you want to support background playback, enable the "Audio, AirPlay, and Picture in Picture" background mode in your project settings.
3. 临时文件管理
- Temporary files in
NSTemporaryDirectory()can be deleted by iOS when the app is not running, but it's good practice to delete them manually when you're done with them to save storage.
4. 错误处理
Add more robust error handling for all audio operations (session setup, recording, playback) to give users clear feedback if something goes wrong.
5. UI/UX
- Update the button's appearance (e.g., color) when recording is active to make it clear to the user.
- Add a loading state or indicator if there's a delay before playback starts.
内容的提问来源于stack exchange,提问作者kruti




