在后台使用AVAudioPlayer
在我的应用程序中,我添加了后台音频和后台处理功能 我的代码目前使用在后台使用AVAudioPlayer,audio,swiftui,avaudioplayer,combine,Audio,Swiftui,Avaudioplayer,Combine,在我的应用程序中,我添加了后台音频和后台处理功能 我的代码目前使用AVAudioPlayer播放音频。当应用程序在前台,屏幕锁定时,播放效果很好,但音频有一些静态抖动 我的应用程序是使用SwiftUI和Combine编写的。是否有人遇到过此问题,您建议如何解决 以下是play方法: /// Play an `AudioFile` /// - Parameters: /// - audioFile: an `AudioFile` struct /// - c
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, 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()
}
}
import-AVFoundation
进口联合收割机
进口基金会
///一个“Combine”-对“AVAudioPlayer”友好的包装器,它使用“Combine”“publisher”而不是“AVAudioPlayerDelegate”`
类CombineAudioPlayer:NSObject、AvaudioPlayerLegate、ObservableObject{
静态let sharedInstance=CombineAudioPlayer()
私有var audioPlayer=AVAudioPlayer()
/*
现在,我要让计时器一直开着,但需要改进
因为它会在网络上生成大量的数据
当前间隔。
*/
//马克:出版商
private var timer=timer.publish(每隔:0.1,
在:RunLoop.main上,
in:RunLoop.Mode.default).autoconnect()
@已发布的公共音频文件:音频文件?
公共变量显示=CurrentValueSubject(假)
公共变量currentTime=PassthroughSubject()
public var didFinishPlayingCurrentAudioFile=PassthroughSubject()
私有变量可取消项:Set=[]
//标记:-初始值设定项
私有重写init(){
super.init()
//设置一个空白的音频文件
发行商()
音频播放器。设置音量(1.0,音量:0)
}
//标记:-发布者方法
私有函数设置发布者(){
timer.sink(receiveCompletion:{completion in
//托多:看看我这里是否需要什么
//不要这样认为,因为这将始终被初始化
},
receiveValue:{中的值
self.isplay.send(self.audioPlayer.isplay)
self.currentTime.send(self.currentTimeValue)
})
.store(在:&可取消项中)
didFinishPlayingCurrentAudioFile.sink(接收完成:{uu}in
},
receiveValue:{audioFile in
self.resetPublishedValues()
})
.store(在:&可取消项中)
}
专用函数setupCurrentAudioFilePublisher(){
self.isplay.send(false)
self.currentTime.send(0.0)
}
//MARK:-播放方法
///播放音频文件`
///-参数:
///-audioFile:audioFile结构
///-完成:可选完成,默认为“零”`
func play(uu音频文件:音频文件,
完成:(()->无效)?=无){
如果音频文件!=当前音频文件{
resetPublishedValues()
}
currentAudioFile=音频文件
setupCurrentAudioFilePublisher()
guard let path=Bundle.main.path(forResource:audioFile.filename,of type:“mp3”)else{
回来
}
让url=url(fileURLWithPath:path)
//每个人都是STFU
停止()
做{
//确保声音是一个
尝试AVAudioSession.sharedInstance().setCategory(.playback,mode:.默认值)
尝试AVAudioSession.sharedInstance().setActive(true)
//实例化AVAudioPlayer的实例
audioPlayer=尝试AVAudioPlayer(内容:url)
audioPlayer.prepareToPlay()
//播放声音
let queue=DispatchQueue(标签:“audioPlayer”,qos:.userInitiated,属性:.concurrent,autoreleaseFrequency:.inherit,目标:nil)
queue.async{
self.audioPlayer.play()
}
audioPlayer.delegate=self
}抓住{
//如果我们处理错误,需要进行“抛出”
打印(字符串(格式:“播放错误:%@”,错误.localizedDescription))
}
}
函数停止(){
音频播放器。停止()
resetPublishedValues()
}
私有函数resetPublishedValues(){
isplay.send(错误)
currentTime.send(0.0)
}
私有变量currentTimeValue:时间间隔{
audioPlayer.currentTime
}
///使用“Publisher”确定声音何时播放完毕。
///-参数:
///-player:AVAudioPlayer实例
///-标志:表示声音是否成功播放的“Bool”
func audioPlayerDidFinishPlaying(uPlayer:AVAudioPlayer,成功标记:Bool){
如果让currentAudioFile=currentAudioFile{
didFinishPlayingCurrentAudioFile.send(currentAudioFile)
}
resetPublishedValues()
}
}
所以我找到了答案。我有几个问题要处理。基本上,我需要在应用程序处于后台的特定时间播放音频文件。当应用程序处于活动状态时,如果声音正在播放,则此操作正常,AVAudioPlayer
将不允许我在应用程序处于后台后启动某些内容,前提是音频播放尚未进行
我不会深入讨论细节,但我最终使用了AVQueuePlayer
,我将其初始化为CombineAudioPlayer类的一部分
AppDelegate
的didfishlaunchingwithoptions
方法中添加了以下几行
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)
Th
private var bgAudioPlayer = 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))
}
}