问题描述
我的代码目前使用 AVAudioPlayer
播放音频。虽然当应用在前台时播放效果很好,但在锁定屏幕的情况下,音频会有一些静态抖动。
我的应用程序是使用 SwiftUI 和 Combine 编写的。有没有人遇到过这个问题,你有什么建议作为解决方法?
这是 play
方法:
/// Play an `AudioFile`
/// - Parameters:
/// - audioFile: an `AudioFile` struct
/// - completion: optional completion,default is `nil`
func play(_ audioFile: AudioFile,completion: (() -> Void)? = nil) {
if audioFile != currentAudioFile {
resetPublishedValues()
}
currentAudioFile = audioFile
setupCurrentAudioFilePublisher()
guard let path = Bundle.main.path(forResource: audioFile.filename,ofType: "mp3") else {
return
}
let url = URL(fileURLWithPath: path)
// everybody STFU
stop()
do {
// make sure the sound is one
try AVAudioSession.sharedInstance().setCategory(.playback,mode: .default)
try AVAudioSession.sharedInstance().setActive(true)
// instantiate instance of AVAudioPlayer
audioPlayer = try AVAudioPlayer(contentsOf: url)
audioPlayer.preparetoPlay()
// play the sound
let queue = dispatchQueue(label: "audioPlayer",qos: .userInitiated,attributes: .concurrent,autoreleaseFrequency: .inherit,target: nil)
queue.async {
self.audioPlayer.play()
}
audioPlayer.delegate = self
} catch {
// Not much to go wrong,so leaving alone for Now,but need to make `throws` if we handle errors
print(String(format: "play() error: %@",error.localizedDescription))
}
}
这是类定义:
import AVFoundation
import Combine
import Foundation
/// A `Combine`-friendly wrapper for `AVAudioPlayer` which utilizes `Combine` `Publishers` instead of `AVAudioPlayerDelegate`
class CombineAudioPlayer: NSObject,AVAudioPlayerDelegate,ObservableObject {
static let sharedInstance = CombineAudioPlayer()
private var audioPlayer = AVAudioPlayer()
/*
FIXME: For Now,gonna leave this timer on all the time,but need to refine
down the road because it's going to generate a fuckload of data on the
current interval.
*/
// MARK: - Publishers
private var timer = Timer.publish(every: 0.1,on: RunLoop.main,in: RunLoop.Mode.default).autoconnect()
@Published public var currentAudioFile: AudioFile?
public var isPlaying = CurrentValueSubject<Bool,Never>(false)
public var currentTime = PassthroughSubject<TimeInterval,Never>()
public var didFinishPlayingCurrentAudioFile = PassthroughSubject<AudioFile,Never>()
private var cancellables: Set<AnyCancellable> = []
// MARK: - Initializer
private override init() {
super.init()
// set it up with a blank audio file
setupPublishers()
audioPlayer.setVolume(1.0,fadeDuration: 0)
}
// MARK: - Publisher Methods
private func setupPublishers() {
timer.sink(receiveCompletion: { completion in
// Todo: figure out if I need anything here
// Don't think so,as this will always be initialized
},receiveValue: { value in
self.isPlaying.send(self.audioPlayer.isPlaying)
self.currentTime.send(self.currentTimeValue)
})
.store(in: &cancellables)
didFinishPlayingCurrentAudioFile.sink(receiveCompletion: { _ in
},receiveValue: { audioFile in
self.resetPublishedValues()
})
.store(in: &cancellables)
}
private func setupCurrentAudioFilePublisher() {
self.isPlaying.send(false)
self.currentTime.send(0.0)
}
// MARK: - Playback Methods
/// Play an `AudioFile`
/// - Parameters:
/// - audioFile: an `AudioFile` struct
/// - completion: optional completion,target: nil)
queue.async {
self.audioPlayer.play()
}
audioPlayer.delegate = self
} catch {
// Need to make `throws` if we handle errors
print(String(format: "play error: %@",error.localizedDescription))
}
}
func stop() {
audioPlayer.stop()
resetPublishedValues()
}
private func resetPublishedValues() {
isPlaying.send(false)
currentTime.send(0.0)
}
private var currentTimeValue: TimeInterval {
audioPlayer.currentTime
}
/// Use the `Publisher` to determine when a sound is done playing.
/// - Parameters:
/// - player: an `AVAudioPlayer` instance
/// - flag: a `Bool` indicating whether the sound was successfully played
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer,successfully flag: Bool) {
if let currentAudioFile = currentAudioFile {
didFinishPlayingCurrentAudioFile.send(currentAudioFile)
}
resetPublishedValues()
}
}
解决方法
所以我明白了。我有几个问题需要解决。基本上,我需要在应用程序处于后台时的特定时间播放音频文件。如果在应用程序处于活动状态时播放声音,这可以正常工作,但如果音频播放尚未进行,AVAudioPlayer
不会让我在应用程序处于后台后启动某些操作。
我不会深入细节,但我最终使用了 AVQueuePlayer
,我将其初始化为我的 CombineAudioPlayer 类的一部分。
- 更新 AppDelegate.swift
我在 AppDelegate
的 didFinishLaunchingWithOptions
方法中添加了以下几行。
func application(_ application: UIApplication,didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
do {
try AVAudioSession.sharedInstance().setCategory(.playback,mode: .default)
try AVAudioSession.sharedInstance().setActive(true)
} catch {
print(String(format: "didFinishLaunchingWithOptions error: %@",error.localizedDescription))
}
return true
}
- 在我的
AudioPlayer
类中,我声明了一个AVQueuePlayer
。使用AudioPlayer
类而不是在方法内部进行初始化至关重要。
我的 ViewModel
订阅了一个通知,该通知侦听应用即将退出前台,它会快速生成一个播放列表并在应用退出之前触发它。
NotificationCenter.default.publisher(for: UIApplication.willResignActiveNotification).sink { _ in
self.playBackground()
}
.store(in: &cancellables)
private var bgAudioPlayer = AVQueuePlayer()
然后,我创建了一个方法来为 AVQueuePlayer 生成一个播放列表,如下所示:
func backgroundPlaylist(from audioFiles: [AudioFile]) -> [AVPlayerItem] {
guard let firstFile = audioFiles.first else {
// return empty array,don't wanna unwrap optionals
return []
}
// declare a silence file
let silence = AudioFile(displayName: "Silence",filename: "1sec-silence")
// start at zero
var currentSeconds: TimeInterval = 0
var playlist: [AVPlayerItem] = []
// while currentSeconds is less than firstFile's fire time...
while currentSeconds < firstFile.secondsInFuture {
// add 1 second of silence to the playlist
playlist.append(AVPlayerItem(url: silence.url!))
// increment currentSeconds and we loop over again,adding more silence
currentSeconds += 1
}
// once we're done,add the file we want to play
playlist.append(AVPlayerItem(url: audioFiles.first!.url!))
return playlist
}
最后播放的声音如下:
func playInBackground() {
do {
// make sure the sound is one
try AVAudioSession.sharedInstance().setCategory(.playback,mode: .default,policy: .longFormAudio,options: [])
try AVAudioSession.sharedInstance().setActive(true)
let playlist = backgroundPlaylist(from: backgroundPlaylist)
bgAudioPlayer = AVQueuePlayer(items: playlist)
bgAudioPlayer.play()
} catch {
// Not much to mess up,so leaving alone for now,but need to make
// `throws` if we handle errors
print(String(format: "playInBackground error: %@",error.localizedDescription))
}
}