在后台使用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.swift
  • 我在
    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
    类而不是在方法内部初始化,这一点至关重要
  • My
    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))
        }
    }