在后台使用 AVAudioPlayer
Using AVAudioPlayer in background
在我的应用程序中,我添加了后台音频和后台处理功能。
我的代码目前使用 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))
}
}
这是 class 定义:
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, 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 {
// 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 class.
的一部分
- 更新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
class 中,我声明了一个 AVQueuePlayer
。使用 AudioPlayer
class 而非方法内部进行初始化非常重要。
我的 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))
}
}
在我的应用程序中,我添加了后台音频和后台处理功能。
我的代码目前使用 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))
}
}
这是 class 定义:
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, 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 {
// 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 class.
- 更新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
class 中,我声明了一个AVQueuePlayer
。使用AudioPlayer
class 而非方法内部进行初始化非常重要。
我的 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))
}
}