Swift AVFoundation问题:切换AudioSession引发动画卡顿
解决训练App提示音播放时动画与声音延迟+背景音恢复的矛盾问题
问题场景与矛盾点
- 开发的训练类App支持用户设置训练间隔时长(如30秒)
- 核心需求:训练启动后运行动画,计时结束时需同步播放提示音、重置动画,同时临时压低其他App的背景音乐
- 遇到的两难问题:
- 原代码播放提示音时,动画会卡在轮结束状态,近1秒后才出声音,之后才重启动画,延迟明显
- 若删掉
try audioSession.setCategory(.ambient, options: .mixWithOthers)这行,延迟消失,但其他App的背景音乐会一直保持压低,无法自动恢复
原问题代码
计时器与动画视图代码
ZStack{ Circle() .trim(from: 0, to: 1) .stroke(colorScheme == .dark ? Color.white.opacity(0.08) : Color.black.opacity(0.09), style: StrokeStyle(lineWidth: 35, lineCap: .round)) .frame(width: x - x/7, height: y - y/7) Circle() .trim(from: 0, to: intervallTimer.position) .stroke(intervallTimer.ringColor.convertToColor(), style: StrokeStyle(lineWidth: 35, lineCap: .round)) .frame(width: x - x/7, height: y - y/7) .rotationEffect(.init(degrees: -90)) VStack(spacing: 0){ intervallTimer.time.inTrainingsFormat() Text(intervallTimer.currentEx) .font(.system(size: 40)) .padding(.top, 20) .multilineTextAlignment(.center) .lineLimit(3) } } .onAppear{ self.timer = Timer.publish(every: 0.1, on: .main, in: .common).autoconnect() } .onReceive(timer){ _ in Sound.playSounds(sound.wav) intervallTimer.position = 0.01 }
初始提示音播放代码(存在延迟问题)
class Sound{ static var audioPlayer:AVAudioPlayer? static func playSounds(soundfile: String) { if let path = Bundle.main.path(forResource: soundfile, ofType: nil){ let audioSession = AVAudioSession.sharedInstance() do{ try audioSession.setCategory(.ambient, options: .duckOthers) audioPlayer = try AVAudioPlayer(contentsOf: URL(fileURLWithPath: path)) audioPlayer?.prepareToPlay() DispatchQueue.global(qos: .background).async { audioPlayer?.play() } try audioSession.setCategory(.ambient, options: .mixWithOthers) //移除该行则无延迟,但背景音保持压低 }catch { print("Error") } } } }
解决方案与优化后代码
核心优化思路
- 提前初始化并预加载音频播放器,避免播放时才创建资源导致的延迟
- 通过
AVAudioPlayerDelegate监听音频播放完成事件,在播放结束后再恢复音频会话,而非立即修改类别 - 切换到
.playback音频类别配合.duckOthers选项,更适配主动播放提示音的场景 - 用
setActive(false, options: .notifyOthersOnDeactivation)在音频结束后通知其他App恢复音量
优化后的Sound类代码
class Sound: NSObject, AVAudioPlayerDelegate{ var audioPlayer: AVAudioPlayer? let audioSession = AVAudioSession.sharedInstance() override init() { super.init() } // 提前准备音频播放器,建议在App启动或进入训练页面时调用 func prepareAudioPlayer(soundfile: String){ if let path = Bundle.main.path(forResource: soundfile, ofType: nil){ do{ try audioSession.setCategory(.playback, options: .duckOthers) audioPlayer = try AVAudioPlayer(contentsOf: URL(fileURLWithPath: path)) audioPlayer?.delegate = self audioPlayer?.prepareToPlay() try? AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation) }catch{ print("Error - Coudnt create audioplayer") } }else{ print("Error - Path not found") } } // 播放提示音,直接调用即可 func play() { if let player = audioPlayer{ player.play() }else{ print("Error - AudioPlayer is nil") } } // 音频播放完成后恢复音频会话,让其他App的背景音恢复正常 func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) { print("Did finish Playing") DispatchQueue.global(qos: .userInitiated).async { try? self.audioSession.setActive(false, options: .notifyOthersOnDeactivation) } } }
计时器部分的调用调整
需要提前初始化Sound实例并完成音频准备,比如在视图的onAppear中处理:
@State private var soundPlayer = Sound() // ... 在视图的onAppear中添加 .onAppear{ self.timer = Timer.publish(every: 0.1, on: .main, in: .common).autoconnect() soundPlayer.prepareAudioPlayer(soundfile: "sound.wav") // 提前加载音频资源 } .onReceive(timer){ _ in soundPlayer.play() // 直接触发播放 intervallTimer.position = 0.01 }
内容的提问来源于stack exchange,提问作者Lukas
相关产品推荐
相关产品推荐

