AVMutableComposition偶尔无声:Swift音频循环insertTimeRange异常求助
Swift音频循环偶尔出现insertTimeRange异常的排查与修复
问题重现
开发音频循环功能时,多数场景下insertTimeRange运行正常,但约每10次会出现一次异常:播放器显示的合成时长正确,但每次循环仅截取极短的可听片段,其余时段为静音,且无任何错误抛出。原实现代码如下:
private func createComposition(audioURL: URL, minutesToLoop: UInt) -> AVMutableComposition? { // Initiate new composition let composition = AVMutableComposition() // Add one audio track (channel) to the composition let compositionAudioTrack: AVMutableCompositionTrack? = composition.addMutableTrack(withMediaType: AVMediaType.audio, preferredTrackID: 0) // Create AVAsset from audio file (mp3 -> React Native bundle string -> URL -> AVAsset) let asset = AVURLAsset(url: audioURL) // Extract (now compatible) audio track from AVAsset let track = asset.tracks(withMediaType: AVMediaType.audio)[0] // Create a time range from 0 - 60 seconds that we can use to cut that out from the track let timeRange = CMTimeRangeMake(start: .zero, duration: CMTimeMakeWithSeconds(60, preferredTimescale: 600)) if (compositionAudioTrack != nil) { // Repeat for as many minutes as user specified in the app for _ in 0...(minutesToLoop - 1) { do { // Take first 60 seconds from the audio file (timeRange, of: track) and paste over and over again exactly at the end of the track (at: composition.duration) try compositionAudioTrack!.insertTimeRange(timeRange, of: track, at: composition.duration) } catch { print("FAILED TO MERGE AUDIO") return nil } } } return composition }
核心原因分析
- AVAsset未完成异步加载:直接初始化
AVURLAsset后就获取轨道,此时asset可能还未完成元数据加载,导致轨道的时间信息不准确,插入时出现隐性异常。 - 时间刻度不匹配:手动指定
preferredTimescale: 600创建时间范围,与原音频轨道的naturalTimeScale不一致,导致时间范围解析出现偏差。 - 线程安全问题:如果该函数在非主线程执行,AVFoundation的部分操作可能因线程冲突出现无报错的异常行为。
修复方案与代码优化
关键修改点
- 异步等待AVAsset加载完成,确保轨道元数据可用
- 使用原轨道的时间刻度创建时间范围,避免刻度不兼容
- 采用异步回调形式,适配AVAsset的异步加载特性
优化后代码
private func createComposition(audioURL: URL, minutesToLoop: UInt, completion: @escaping (AVMutableComposition?) -> Void) { let composition = AVMutableComposition() guard let compositionAudioTrack = composition.addMutableTrack(withMediaType: .audio, preferredTrackID: 0) else { completion(nil) return } let asset = AVURLAsset(url: audioURL) // 异步等待asset的轨道数据加载完成 asset.loadValuesAsynchronously(forKeys: ["tracks"]) { var loadError: NSError? let loadStatus = asset.statusOfValue(forKey: "tracks", error: &loadError) guard loadStatus == .loaded else { print("Asset加载失败: \(loadError?.localizedDescription ?? "未知错误")") completion(nil) return } guard let sourceTrack = asset.tracks(withMediaType: .audio).first else { print("未找到音频轨道") completion(nil) return } // 使用原轨道的时间刻度创建60秒时间范围,保证时间计算准确 let sixtySeconds = CMTimeMakeWithSeconds(60, preferredTimescale: sourceTrack.naturalTimeScale) let loopTimeRange = CMTimeRange(start: .zero, duration: sixtySeconds) do { // 循环插入音频片段,改用半开区间避免边界错误 for _ in 0..<minutesToLoop { try compositionAudioTrack.insertTimeRange(loopTimeRange, of: sourceTrack, at: composition.duration) } completion(composition) } catch { print("合并音频失败: \(error.localizedDescription)") completion(nil) } } }
说明
- 异步加载:通过
loadValuesAsynchronously等待asset的tracks属性加载完成,确保获取的轨道数据是完整的。 - 刻度匹配:使用
sourceTrack.naturalTimeScale创建时间范围,保证与原音频的时间单位一致,避免时间转换误差。 - 回调设计:将函数改为异步回调形式,因为AVAsset加载是异步操作,同步返回可能导致数据未就绪。
内容的提问来源于stack exchange,提问作者ejanson
相关产品推荐
相关产品推荐

