Deactivate AudioSession in AudioKit - swift

I'm using AudioKit, and trying to set audio session inactive, when I don't need it. So, I wrote some simple code to understand mechanism of this process, and faced one unexpectable trouble. With attempts of deactivating session, I'm getting error:
[avas] AVAudioSession.mm:1079:-[AVAudioSession setActive:withOptions:error:]: Deactivating an audio session that has running I/O. All I/O should be stopped or paused prior to deactivating the audio session.
ViewController.swift:deactivateAudioSession():73:Error Domain=NSOSStatusErrorDomain Code=560030580 "(null)"
This is my sample code:
import UIKit
import AudioKit
class ViewController: UIViewController {
var reverb: AKReverb?
var delay: AKDelay?
var chorus: AKChorus?
var mic: AKMicrophone!
override func viewDidLoad() {
NotificationCenter.default.addObserver(self, selector: #selector(applicationWillResignActive),
name: UIApplication.willResignActiveNotification,
object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(applicationDidBecomeActive),
name: UIApplication.didBecomeActiveNotification,
object: nil)
super.viewDidLoad()
AKSettings.useBluetooth = true
guard let mic = AKMicrophone() else { return }
self.mic = mic
chorus = AKChorus(mic)
chorus?.depth = 1
chorus?.frequency = 44000
let delay = AKDelay(chorus)
delay.feedback = 0.2
reverb = AKReverb(delay)
let mix = AKMixer(reverb)
AudioKit.output = mix
}
#IBAction func launchEngine() {
do {
try AudioKit.start()
} catch {
AKLog(error)
}
}
#IBAction func deactivateAudioSession() {
// mic.stop()
// reverb?.stop()
// delay?.stop()
// chorus?.stop()
do {
try AKSettings.session.setActive(false)
} catch {
AKLog(error)
}
}
#objc func applicationDidBecomeActive() {
launchEngine()
}
#objc func applicationWillResignActive() {
deactivateAudioSession()
}
}
As you can see, basically, I wanted to handle app state changing, but this error happens even if I call deactivateAudioSession() method using UI. I've tried to stop() all node objects (commented code) before deactivation, but error stays. What I'm doing wrong?

Related

How to do Apple Watch Background App Refresh?

I've consulted many variations of background app refresh for Apple Watch so that I can update the complications for my app. However, the process seems very much hit or miss and most of the time it doesn't run at all after some time.
Here is the code I currently have:
BackgroundService.swift
Responsibility: Schedule background refresh and handle download processing and update complications.
import Foundation
import WatchKit
final class BackgroundService: NSObject, URLSessionDownloadDelegate {
var isStarted = false
private let requestFactory: RequestFactory
private let logManager: LogManager
private let complicationService: ComplicationService
private let notificationService: NotificationService
private var pendingBackgroundTask: WKURLSessionRefreshBackgroundTask?
private var backgroundSession: URLSession?
init(requestFactory: RequestFactory,
logManager: LogManager,
complicationService: ComplicationService,
notificationService: NotificationService
) {
self.requestFactory = requestFactory
self.logManager = logManager
self.complicationService = complicationService
self.notificationService = notificationService
super.init()
NotificationCenter.default.addObserver(self,
selector: #selector(handleInitialSchedule(_:)),
name: Notification.Name("ScheduleBackgroundTasks"),
object: nil
)
}
func updateContent() {
self.logManager.debugMessage("In BackgroundService updateContent")
let complicationsUpdateRequest = self.requestFactory.makeComplicationsUpdateRequest()
let config = URLSessionConfiguration.background(withIdentifier: "app.wakawatch.background-refresh")
config.isDiscretionary = false
config.sessionSendsLaunchEvents = true
self.backgroundSession = URLSession(configuration: config,
delegate: self,
delegateQueue: nil)
let backgroundTask = self.backgroundSession?.downloadTask(with: complicationsUpdateRequest)
backgroundTask?.resume()
self.isStarted = true
self.logManager.debugMessage("backgroundTask started")
}
func handleDownload(_ backgroundTask: WKURLSessionRefreshBackgroundTask) {
self.logManager.debugMessage("Handling finished download")
self.pendingBackgroundTask = backgroundTask
}
func urlSession(_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didFinishDownloadingTo location: URL) {
processFile(file: location)
self.logManager.debugMessage("Marking pending background tasks as completed.")
if self.pendingBackgroundTask != nil {
self.pendingBackgroundTask?.setTaskCompletedWithSnapshot(false)
self.backgroundSession?.invalidateAndCancel()
self.pendingBackgroundTask = nil
self.backgroundSession = nil
self.logManager.debugMessage("Pending background task cleared")
}
self.schedule()
}
func processFile(file: URL) {
guard let data = try? Data(contentsOf: file) else {
self.logManager.errorMessage("file could not be read as data")
return
}
guard let backgroundUpdateResponse = try? JSONDecoder().decode(BackgroundUpdateResponse.self, from: data) else {
self.logManager.errorMessage("Unable to decode response to Swift object")
return
}
let defaults = UserDefaults.standard
defaults.set(backgroundUpdateResponse.totalTimeCodedInSeconds,
forKey: DefaultsKeys.complicationCurrentTimeCoded)
self.complicationService.updateTimelines()
self.notificationService.isPermissionGranted(onGrantedHandler: {
self.notificationService.notifyGoalsAchieved(newGoals: backgroundUpdateResponse.goals)
})
self.logManager.debugMessage("Complication updated")
}
func schedule() {
let time = self.isStarted ? 15 * 60 : 60
let nextInterval = TimeInterval(time)
let preferredDate = Date.now.addingTimeInterval(nextInterval)
WKExtension.shared().scheduleBackgroundRefresh(withPreferredDate: preferredDate,
userInfo: nil) { error in
if error != nil {
self.logManager.reportError(error!)
return
}
self.logManager.debugMessage("Scheduled for \(preferredDate)")
}
}
#objc func handleInitialSchedule(_ notification: NSNotification) {
if !self.isStarted {
self.schedule()
}
}
deinit {
NotificationCenter.default.removeObserver(self)
}
}
The flow for the above file's usage is that it will be used by the ExtensionDelegate to schedule background refresh. The first time, it'll schedule a refresh for 1 minute out and then every 15 minutes after that.
Here is the ExtensionDelegate:
import Foundation
import WatchKit
class ExtensionDelegate: NSObject, WKExtensionDelegate {
private var backgroundService: BackgroundService?
private var logManager: LogManager?
override init() {
super.init()
self.backgroundService = DependencyInjection.shared.container.resolve(BackgroundService.self)!
self.logManager = DependencyInjection.shared.container.resolve(LogManager.self)!
}
func isAuthorized() -> Bool {
let defaults = UserDefaults.standard
return defaults.bool(forKey: DefaultsKeys.authorized)
}
func applicationDidFinishLaunching() {
self.logManager?.debugMessage("In applicationDidFinishLaunching")
if isAuthorized() && !(self.backgroundService?.isStarted ?? false) {
self.backgroundService?.schedule()
}
}
func handle(_ backgroundTasks: Set<WKRefreshBackgroundTask>) {
self.logManager?.debugMessage("In handle backgroundTasks")
if !isAuthorized() {
return
}
for task in backgroundTasks {
self.logManager?.debugMessage("Processing task: \(task.debugDescription)")
switch task {
case let backgroundTask as WKApplicationRefreshBackgroundTask:
self.backgroundService?.updateContent()
backgroundTask.setTaskCompletedWithSnapshot(false)
case let urlSessionTask as WKURLSessionRefreshBackgroundTask:
self.backgroundService?.handleDownload(urlSessionTask)
default:
task.setTaskCompletedWithSnapshot(false)
}
}
}
}
When the app is launched from background, it'll try to schedule for the first time in applicationDidFinishLaunching.
From my understanding, WKExtension.shared().scheduleBackgroundRefresh will get called in schedule, then after the preferred time WatchOS will call handle with WKApplicationRefreshBackgroundTask task. Then I will use that to schedule a background URL session task and immediately start it as seen in the updateContent method of BackgroundService. After some time, WatchOS will then call ExtensionDelegate's handle method with WKURLSessionRefreshBackgroundTask and I'll handle that using the handleDownload task. In there, I process the response, update the complications, clear the task, and finally schedule a new background refresh.
I've found it works great if I'm actively working on the app or interacting with it in general. But let's say I go to sleep then the next day the complication will not have updated at all.
Ideally, I'd like for it to function as well as the Weather app complication WatchOS has. I don't interact with the complication, but it reliably updates.
Is the above process correct or are there any samples of correct implementations?
Some of the posts I've consulted:
https://wjwickham.com/posts/refreshing-data-in-the-background-on-watchOS/
https://developer.apple.com/documentation/watchkit/background_execution/using_background_tasks
https://spin.atomicobject.com/2021/01/26/complications-basic-functionality/

Swift: Ignored request to start a new background task because RunningBoard has already started the expiration timer

I have a series of tasks that is triggered by a silent push notification. Upon receiving the push notification, it wakes the iOS up in the background and performs the following tasks:
Opens up a WebViewController that contains a WKWebview
Goes to a webpage, and clicks some buttons automated by javascript injection
Once completed, dismisses the WebViewController
I have added selected BackgroundTasks handlers to manage it by following this tutorial but the console is flooded with the following warning.
[ProcessSuspension] 0x280486080 - WKProcessAssertionBackgroundTaskManager: Ignored request to start a new background task because RunningBoard has already started the expiration timer
Note that the tasks that needs to be done are still performed correctly.
class WebViewController: UIViewController, WKNavigationDelegate, WKScriptMessageHandler {
lazy var webView: WKWebView = {
let v = WKWebView()
v.translatesAutoresizingMaskIntoConstraints = false
v.navigationDelegate = self
return v
}()
var backgroundTask: UIBackgroundTaskIdentifier = .invalid
//Remove BG task when not needed
deinit {
NotificationCenter.default.removeObserver(self)
endBackgroundTask()
}
override func viewDidLoad() {
super.viewDidLoad()
//Register notification for background task
NotificationCenter.default.addObserver(self,
selector: #selector(reinstateBackgroundTask),
name: UIApplication.didBecomeActiveNotification,
object: nil)
registerBackgroundTask()
//Load webview with URL
if let url = url {
let request = URLRequest(url: url)
webView.load(request)
}
}
//MARK:- Handle BG Tasks
func registerBackgroundTask() {
backgroundTask = UIApplication.shared.beginBackgroundTask { [weak self] in
self?.endBackgroundTask()
}
}
func endBackgroundTask() {
Log("Background task ended.")
UIApplication.shared.endBackgroundTask(backgroundTask)
backgroundTask = .invalid
}
#objc func reinstateBackgroundTask() {
if backgroundTask == .invalid {
registerBackgroundTask()
}
}
func endBackgroundTaskIfNotInvalid() {
if backgroundTask != .invalid {
self.endBackgroundTask()
}
}
//This is the final task that needs to be done
fileprivate func updateScheduler(visitedPlace: VisitedPlace) {
if navigator == .scheduler {
if let jobId = jobId {
let data = [
"status": "scheduled",
"completedOn": Date()
] as [String : Any]
///Do some work here...
//Dismiss controller after completing
self.dismiss(animated: true) {
self.endBackgroundTaskIfNotInvalid()
}
}
} else {
self.endBackgroundTaskIfNotInvalid()
}
}
}
What is triggering all these warnings and how do I silence it?
I'm having the same console flood. For me it turned out to be adMob that was the cause.
This happens to me when I run unit tests that wait for test expectations to be filled. I was hoping that it was just a simulator issue, since I don't see it in production, but it sounds like that's not the case.
I fixed the flood by removing the AdMob banner from the view hierarchy on app suspension:
self.bannerView?.removeFromSuperview()
AdMob would still try infrequently which would generate a single log message.

Basic Sinch Sample in Swift - but no Sound

first of all thank you for reading my lines.
For an idea I'm currently trying to dive into the Swift world (I only have very basic programming knowledge - no Objective C knowledge
).
I tried to set up the following lines to create a very basic app-to-app sample in Sinch. After my code I let you know what the issues are.
import UIKit
import Sinch
var appKey = "APP_KEY_FROM_MY_ACCOUNT"
var hostname = "clientapi.sinch.com"
var secret = "SECRET_FROM_MY_ACCOUNT"
class CViewController: UIViewController, SINCallClientDelegate, SINCallDelegate, SINClientDelegate {
var client: SINClient?
var call: SINCall?
var audio: SINAudioController?
//Text field in the main storyboard
#IBOutlet weak var userNameSepp: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
self.initSinchClient()
}
//initialize and start the client as a fixed "userA"
func initSinchClient() {
client = Sinch.client(withApplicationKey: appKey, applicationSecret: secret, environmentHost: hostname, userId: "userB")
client?.call().delegate = self
client?.delegate = self
client?.startListeningOnActiveConnection()
client?.setSupportCalling(true)
client?.start()
}
//Did the Client start?
func clientDidStart(_ client: SINClient!) {
print("Hello")
}
//Did the Client fail?
func clientDidFail(_ client: SINClient!, error: Error!) {
print("Good Bye")
}
//Call Button in the main.storyboard ... if call==nil do the call ... else hangup and set call to nil
//the background color changes are my "debugging" :D
#IBAction func callSepp(_ sender: Any) {
if call == nil{
call = client?.call()?.callUser(withId: userNameSepp.text)
//for testing I change to callPhoneNumber("+46000000000").
// the phone call progresses (but I hear nothing),
// the phonecall gets established (but I hear nothing)
// and the phonecall gets ended (but of course I hear nothing)
self.view.backgroundColor = UIColor.red
call?.delegate = self
audio = client?.audioController()
}
else{
call?.hangup()
self.view.backgroundColor = UIColor.blue
call = nil
}
}
func callDidProgress(_ call: SINCall?) {
self.view.backgroundColor = UIColor.green
client?.audioController().startPlayingSoundFile("/LONG_PATH/ringback.wav", loop: true)
print("Call in Progress")
}
//I know that this works but I don't hear anything
func callDidEstablish(_ call: SINCall!) {
client?.audioController().stopPlayingSoundFile()
print("Call did Establish")
}
func callDidEnd(_ call: SINCall!) {
print("Call did end")
}
// this works fine
#IBAction func hangUpSepp(_ sender: Any) {
call?.hangup()
self.view.backgroundColor = UIColor.red
call = nil
}
// i work in a "sub view controller" - so i navigate here back to the main view controller
#IBAction func goBackMain(_ sender: Any) {
call?.hangup()
dismiss(animated: true, completion: nil)
client?.stopListeningOnActiveConnection()
client?.terminateGracefully()
client = nil
}
}
So I can call my private phone number or if I change to callUser I can call another app but I don't hear anything. What do I miss? It must have to do with the SINAudioController and the client's method audioController() but I don't know what I'm doing wrong. Thank you for your help.

Unexpectedly found nil for optional value - but optional value is not nil

I'm using AudioKit to set up a sounds player. My sounds engine is set up in a singleton with these functions:
override func viewDidLoad() {
super.viewDidLoad()
configureAudioPlayer()
}
func configureAudioPlayer() {
leftPanner = AKPanner(leftOscillator)
rightPanner = AKPanner(rightOscillator)
//Set up rain and rainPlayer
do {
rain = try AKAudioFile(readFileName: "rain.wav")
rainPlayer = try AKAudioPlayer(file: rain, looping: true, deferBuffering: false, completionHandler: nil)
} catch {
print(error)
}
mixer = AKMixer(leftPanner, rightPanner, rainPlayer)
envelope = AKAmplitudeEnvelope(mixer)
AudioKit.output = envelope
AudioKit.start()
}
func startPlaying() {
leftOscillator.start()
rightOscillator.start()
rainPlayer.start() //CRASHES HERE AND SAYS NIL OPTIONAL VALUE
envelope.start()
soundIsPlaying = true
}
In a separate view controller I have a play button that calls the following function when pressed:
let player = AudioPlayer.sharedPlayer
#IBAction func playSound(_ sender: Any) {
player.leftOscillator.frequency = 220
player.rightOscillator.frequency = 230
if player.soundIsPlaying == false {
player.startPlaying()
} else {
player.stopPlaying()
}
}
When I press this button the app crashes and says that the rainPlayer is nil. Did I set something up wrong? Why would the rainPlayer be nil when I configured it in the configureAudioPlayer() function?
UPDATE:
Moving the singleton into its own class that inherits from NSObject and calling configureAudioPlayer() in its init() function solves my problem. configureAudioPlayer() in the viewDidLoad() method was never being called.

AVAudioRecorder delegate not assigned in Swift

I decided to rewrote audiorecorder class from Objective C to Swift.
In Objective C recording works, but in Swift AVAudioRecorderDelegate delegate methods not called, recorder starts successfully.
How can I fix this?
class VoiceRecorder: NSObject, AVAudioPlayerDelegate, AVAudioRecorderDelegate {
var audioRecorder: AVAudioRecorder!
var audioPlayer: AVAudioPlayer?
override init() {
super.init()
var error: NSError?
let audioRecordingURL = self.audioRecordingPath()
audioRecorder = AVAudioRecorder(URL: audioRecordingURL,
settings: self.audioRecordingSettings(),
error: &error)
audioRecorder.meteringEnabled = true
/* Prepare the recorder and then start the recording */
audioRecorder.delegate = self
if audioRecorder.prepareToRecord(){
println("Successfully prepared for record.")
}
}
func audioRecorderDidFinishRecording(recorder: AVAudioRecorder!, successfully flag: Bool) {
println("stop")
if flag{
println("Successfully stopped the audio recording process")
if completionHandler != nil {
completionHandler(success: flag)
}
} else {
println("Stopping the audio recording failed")
}
}
}
func record(){
audioRecorder.record()
}
//UPD.
func stop(#completion:StopCompletionHandler){
self.completionHandler = completion
self.audioRecorder?.stop
}
The problem is this line:
self.audioRecorder?.stop
That is not a call to the stop method. It merely mentions the name of the method. You want to say this:
self.audioRecorder?.stop()
Those parentheses make all the difference.