Swift中处理后音频的保存与分享实现问题求助
iOS音频保存与分享功能故障排查与修复
问题背景
我正在修改一款iOS应用,功能是文本转语音生成音频,可调整音调和语速,之后支持用户保存并分享处理后的音频。目前录制与音频处理环节正常,但保存和分享功能存在问题。
原代码实现
func saveOutputAudio(rate: Float = 1.0, pitch: Float = 0.0, echo: Bool = false, reverb: Bool = false, completionHandler: (() -> Void)? = nil) { let audioFileURL = getDocumentsDirector().appendingPathComponent(fileName) as URL let audioFile = try! AVAudioFile(forReading: audioFileURL) let audioFormat = audioFile.processingFormat let audioFrameCount = UInt32(audioFile.length) let audioFileBuffer = AVAudioPCMBuffer(pcmFormat: audioFormat, frameCapacity: audioFrameCount)! try! audioFile.read(into: audioFileBuffer) let audioEngine = AVAudioEngine() let audioPlayerNode = AVAudioPlayerNode() audioEngine.attach(audioPlayerNode) let changeRateEffect = AVAudioUnitTimePitch() changeRateEffect.rate = rate audioEngine.attach(changeRateEffect) let changePitchEffect = AVAudioUnitTimePitch() changePitchEffect.pitch = pitch audioEngine.attach(changePitchEffect) let echoEffect = AVAudioUnitDelay() echoEffect.wetDryMix = echo ? 50 : 0 audioEngine.attach(echoEffect) let reverbEffect = AVAudioUnitReverb() reverbEffect.wetDryMix = reverb ? 50 : 0 audioEngine.attach(reverbEffect) audioEngine.connect(audioPlayerNode, to: changeRateEffect, format: audioFormat) audioEngine.connect(changeRateEffect, to: changePitchEffect, format: audioFormat) audioEngine.connect(changePitchEffect, to: echoEffect, format: audioFormat) audioEngine.connect(echoEffect, to: reverbEffect, format: audioFormat) audioEngine.connect(reverbEffect, to: audioEngine.mainMixerNode, format: audioFormat) audioPlayerNode.scheduleBuffer(audioFileBuffer, at: nil, options: .loops, completionHandler: nil) try! audioEngine.start() audioPlayerNode.play() let outputFileURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("outputAudio.m4a") let outputFile = try! AVAudioFile(forWriting: outputFileURL, settings: audioFormat.settings) let outputFileBuffer = AVAudioPCMBuffer(pcmFormat: audioFormat, frameCapacity: audioFrameCount)! var counter = 0 while audioPlayerNode.isPlaying && counter < 2 { if let nextRenderTime = audioPlayerNode.lastRenderTime { let outputBuffer = AVAudioPCMBuffer(pcmFormat: audioFormat, frameCapacity: audioFrameCount)! try! outputFile.write(from: outputBuffer) counter += 1 } else { usleep(10_000) } } try! audioEngine.stop() // Get the audio data from the output file let audioData = try! Data(contentsOf: outputFileURL) // Convert audio data to M4A format let convertedData = convertToM4A(audioData: audioData) // Share the processed audio data let activityViewController = UIActivityViewController(activityItems: [convertedData], applicationActivities: nil) activityViewController.popoverPresentationController?.sourceView = self.view self.present(activityViewController, animated: true) { completionHandler?() } }
问题分析
原代码存在多个关键问题导致保存和分享失败:
- 错误的音频捕获方式:循环创建空
AVAudioPCMBuffer写入文件,未捕获经过音频引擎处理后的有效数据,保存的文件为空或无效。 - 冗余效果器使用:同时用两个
AVAudioUnitTimePitch实例,单个实例即可同时调整速率和音调。 - 无错误处理:大量
try!和强制解包!会在异常时直接崩溃,无法定位问题。 - 不合理的循环逻辑:仅循环2次写入,无法保证完整捕获音频内容,
usleep阻塞主线程影响UI响应。 - 格式配置错误:直接使用原音频
settings创建输出文件,可能不兼容M4A格式,额外的convertToM4A可能导致重复处理或转换失败。
修复方案
以下是修正后的代码,解决了上述所有问题:
func saveOutputAudio(rate: Float = 1.0, pitch: Float = 0.0, echo: Bool = false, reverb: Bool = false, completionHandler: (() -> Void)? = nil) { // 1. 安全读取源音频文件 guard let documentsDir = getDocumentsDirector() else { print("无法获取文档目录") completionHandler?() return } let audioFileURL = documentsDir.appendingPathComponent(fileName) guard let audioFile = try? AVAudioFile(forReading: audioFileURL) else { print("无法读取源音频文件") completionHandler?() return } let audioFormat = audioFile.processingFormat let audioFrameCount = UInt32(audioFile.length) guard let audioFileBuffer = AVAudioPCMBuffer(pcmFormat: audioFormat, frameCapacity: audioFrameCount), try? audioFile.read(into: audioFileBuffer) != nil else { print("无法创建或填充音频缓冲区") completionHandler?() return } // 2. 配置音频引擎和效果器 let audioEngine = AVAudioEngine() let audioPlayerNode = AVAudioPlayerNode() audioEngine.attach(audioPlayerNode) // 单个AVAudioUnitTimePitch同时调整速率和音调 let timePitchEffect = AVAudioUnitTimePitch() timePitchEffect.rate = rate timePitchEffect.pitch = pitch audioEngine.attach(timePitchEffect) let echoEffect = AVAudioUnitDelay() echoEffect.wetDryMix = echo ? 50 : 0 audioEngine.attach(echoEffect) let reverbEffect = AVAudioUnitReverb() reverbEffect.wetDryMix = reverb ? 50 : 0 audioEngine.attach(reverbEffect) // 连接节点 audioEngine.connect(audioPlayerNode, to: timePitchEffect, format: audioFormat) audioEngine.connect(timePitchEffect, to: echoEffect, format: audioFormat) audioEngine.connect(echoEffect, to: reverbEffect, format: audioFormat) audioEngine.connect(reverbEffect, to: audioEngine.mainMixerNode, format: audioFormat) // 3. 配置输出文件(直接生成M4A兼容格式) let outputFileURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("outputAudio.m4a") // 删除已存在的旧文件 try? FileManager.default.removeItem(at: outputFileURL) // 配置AAC编码的M4A输出设置 let outputSettings: [String: Any] = [ AVFormatIDKey: kAudioFormatMPEG4AAC, AVSampleRateKey: audioFormat.sampleRate, AVNumberOfChannelsKey: audioFormat.channelCount, AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue ] guard let outputFile = try? AVAudioFile(forWriting: outputFileURL, settings: outputSettings, commonFormat: .pcmFormatFloat32, interleaved: false) else { print("无法创建输出音频文件") completionHandler?() return } // 4. 安装Tap捕获处理后的音频 let mainMixer = audioEngine.mainMixerNode var isRecordingComplete = false mainMixer.installTap(onBus: 0, bufferSize: 4096, format: mainMixer.outputFormat(forBus: 0)) { buffer, time in guard !isRecordingComplete else { return } do { try outputFile.write(from: buffer) } catch { print("写入音频文件失败: \(error.localizedDescription)") isRecordingComplete = true } } // 5. 播放并等待处理完成 audioPlayerNode.scheduleBuffer(audioFileBuffer, at: nil, completionHandler: { DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { isRecordingComplete = true audioEngine.stop() audioPlayerNode.stop() mainMixer.removeTap(onBus: 0) // 6. 读取输出文件并分享 guard let _ = try? Data(contentsOf: outputFileURL) else { print("无法读取输出音频数据") completionHandler?() return } let activityViewController = UIActivityViewController(activityItems: [outputFileURL], applicationActivities: nil) activityViewController.popoverPresentationController?.sourceView = self.view self.present(activityViewController, animated: true) { completionHandler?() } } }) do { try audioEngine.start() audioPlayerNode.play() } catch { print("音频引擎启动失败: \(error.localizedDescription)") completionHandler?() } }
关键修复点说明
- 使用Tap捕获音频:通过
installTap直接获取经过所有效果器处理后的音频数据,确保写入文件的内容有效。 - 合并效果器实例:单个
AVAudioUnitTimePitch同时设置速率和音调,避免冗余和潜在冲突。 - 添加错误处理:用
guard和try?替代强制解包,捕获并打印错误,避免崩溃并便于调试。 - 异步完成逻辑:通过
scheduleBuffer的回调判断播放完成,延迟0.1秒确保最后一段数据写入,避免丢失内容。 - 直接生成M4A:配置AAC编码的输出设置,无需额外格式转换,分享时传入文件URL提升兼容性。
- 移除阻塞操作:去掉
usleep和循环,使用异步回调处理,避免阻塞主线程影响用户体验。
内容的提问来源于stack exchange,提问作者Sanjug Sonowal
相关产品推荐
相关产品推荐

