SNAudioStreamAnalyzer not stopping sound classification request - swift

I'm a student studying iOS development currently working on a simple AI project that utilizes SNAudioStreamAnalyzer to classify an incoming audio stream from the device's microphone. I can start the stream and analyze audio no problem, but I've noticed I can't seem to get my app to stop analyzing and close the audio input stream when I'm done. At the beginning, I initialize the audio engine and create the classification request like so:
private func startAudioEngine() {
do {
// start the stream of audio data
try audioEngine.start()
let snoreClassifier = try? SnoringClassifier2_0().model
let classifySoundRequest = try audioAnalyzer.makeRequest(snoreClassifier)
try streamAnalyzer.add(classifySoundRequest,
withObserver: self.audioAnalyzer)
} catch {
print("Unable to start AVAudioEngine: \(error.localizedDescription)")
}
}
After I'm done classifying my audio stream, I attempt to stop the audio engine and close the stream like so:
private func terminateNight() {
streamAnalyzer.removeAllRequests()
audioEngine.stop()
stopAndSaveNight()
do {
let session = AVAudioSession.sharedInstance()
try session.setActive(false)
} catch {
print("unable to terminate audio session")
}
nightSummary = true
}
However, after I call the terminateNight() function my app will continue using the microphone and classifying the incoming audio. Here's my SNResultsObserving implementation:
class AudioAnalyzer: NSObject, SNResultsObserving {
var prediction: String?
var confidence: Double?
let snoringEventManager: SnoringEventManager
internal init(prediction: String? = nil, confidence: Double? = nil, snoringEventManager: SnoringEventManager) {
self.prediction = prediction
self.confidence = confidence
self.snoringEventManager = snoringEventManager
}
func makeRequest(_ customModel: MLModel? = nil) throws -> SNClassifySoundRequest {
if let model = customModel {
let customRequest = try SNClassifySoundRequest(mlModel: model)
return customRequest
} else {
throw AudioAnalysisErrors.ModelInterpretationError
}
}
func request(_ request: SNRequest, didProduce: SNResult) {
guard let classificationResult = didProduce as? SNClassificationResult else { return }
let topClassification = classificationResult.classifications.first
let timeRange = classificationResult.timeRange
self.prediction = topClassification?.identifier
self.confidence = topClassification?.confidence
if self.prediction! == "snoring" {
self.snoringEventManager.snoringDetected()
} else {
self.snoringEventManager.nonSnoringDetected()
}
}
func request(_ request: SNRequest, didFailWithError: Error) {
print("ended with error \(didFailWithError)")
}
func requestDidComplete(_ request: SNRequest) {
print("request finished")
}
}
It was my understanding that upon calling streamAnalyzer.removeAllRequests() and audioEngine.stop() the app would stop streaming from the microphone and call the requestDidComplete function, but this isn't the behavior I'm getting. Any help is appreciated!

From OP's edition:
So I've realized it was a SwiftUI problem. I was calling the startAudioEngine() function in the initializer of the view it was declared on. I thought this would be fine, but since this view was embedded in a parent view when SwiftUI updated the parent it was re-initializing my view and as such calling startAudioEngine() again. The solution was to call this function in on onAppear block so that it activates the audio engine only when the view appears, and not when SwiftUI initializes it.

I don't believe you should expect to receive requestDidComplete due to removing a request. You'd expect to receive that when you call completeAnalysis.

Related

How to exit from `RunLoop`

I'm subclassing InputStream from iOS Foundation SDK for my needs. I need to implement functionality that worker thread can sleep until data appear in the stream. The test I'm using to cover the functionality is below:
func testStreamWithRunLoop() {
let inputStream = BLEInputStream() // custom input stream subclass
inputStream.delegate = self
let len = Int.random(in: 0..<100)
let randomData = randData(length: len) // random data generation
let tenSeconds = Double(10)
let oneSecond = TimeInterval(1)
runOnBackgroundQueueAfter(oneSecond) {
inputStream.accept(randomData) // input stream receives the data
}
let dateInFuture = Date(timeIntervalSinceNow: tenSeconds) // time in 10 sec
inputStream.schedule(in: .current, forMode: RunLoop.Mode.default) //
RunLoop.current.run(until: dateInFuture) // wait for data appear in input stream
XCTAssertTrue(dateInFuture.timeIntervalSinceNow > 0, "Timeout. RunLoop didn't exit in 1 sec. ")
}
Here the overriden methods of InputStream
public override func schedule(in aRunLoop: RunLoop, forMode mode: RunLoop.Mode) {
self.runLoop = aRunLoop // save RunLoop object
var context = CFRunLoopSourceContext() // make context
self.runLoopSource = CFRunLoopSourceCreate(nil, 0, &context) // make source
let cfloopMode: CFRunLoopMode = CFRunLoopMode(mode as CFString)
CFRunLoopAddSource(aRunLoop.getCFRunLoop(), self.runLoopSource!, cfloopMode)
}
public func accept(_ data: Data) {
guard data.count > 0 else { return }
self.data += data
delegate?.stream?(self, handle: .hasBytesAvailable)
if let runLoopSource {
CFRunLoopSourceSignal(runLoopSource)
}
if let runLoop {
CFRunLoopWakeUp(runLoop.getCFRunLoop())
}
}
But calling CFRunLoopSourceSignal(runLoopSource) and CFRunLoopWakeUp(runLoop.getCFRunLoop()) not get exit from runLoop.
Does anybody know where I'm mistaking ?
Thanks all!
PS: Here the Xcode project on GitHub
Finally I figured out some issues with my code.
First of all I need to remove CFRunLoopSource object from run loop CFRunLoopRemoveSource(). In according with documentation if RunLoop has no input sources then it exits immediately.
public func accept(_ data: Data) {
guard data.count > 0 else { return }
self.data += data
delegate?.stream?(self, handle: .hasBytesAvailable)
if let runLoopSource, let runLoop, let runLoopMode {
CFRunLoopRemoveSource(runLoop.getCFRunLoop(), runLoopSource, runLoopMode)
}
if let runLoop {
CFRunLoopWakeUp(runLoop.getCFRunLoop())
}
}
Second issue is related that I used XCTest environment and it's RunLoop didn't exit for some reasons (Ask the community for help).
I used real application environment and created Thread subclass to check my implementation. The thread by default has run loop without any input sources attached to it. I added input stream to it. And using main thread emulated that stream received data.
Here the Custom Thread implement that runs and sleep until it receive signal from BLEInputStream
class StreamThread: Thread, StreamDelegate {
let stream: BLEInputStream
init(stream: BLEInputStream) {
self.stream = stream
}
override func main() {
stream.delegate = self
stream.schedule(in: .current, forMode: RunLoop.Mode.default)
print("start()")
let tenSeconds = Double(10)
let dateInFuture = Date(timeIntervalSinceNow: tenSeconds)
RunLoop.current.run(until: dateInFuture)
print("after 10 seconds")
}
override func start() {
super.start()
}
func stream(_ aStream: Stream, handle eventCode: Stream.Event) {
if eventCode == .errorOccurred {
print("eventCode == .errorOccurred")
}
else if eventCode == .hasBytesAvailable {
print("eventCode == .hasBytesAvailable")
}
}
}
Here the some UIViewController methods which runs from main thread
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let baseDate = Date.now
let thread = StreamThread(stream: stream, baseDate: baseDate)
thread.start()
print("main thread pauses at \(Date.now.timeIntervalSince(baseDate))")
Thread.sleep(forTimeInterval: 2)
print("stream accepts Data \(Date.now.timeIntervalSince(baseDate))")
stream.accept(Data([1,2,3]))
}
Here the result:
Everything works as expected - the thread sleeps until input stream receive data. No processor resources consuming.
Although it's allowed to subclass InputStream, there is no good explanation in the documentation how to correctly implement custom InputStream

How to set NowPlaying properties with a AVQueuePlayer in Swift?

I have an AVQueuePlayer that gets songs from a Firebase Storage via their URL and plays them in sequence.
static func playQueue() {
for song in songs {
guard let url = song.url else { return }
lofiSongs.append(AVPlayerItem(url: url))
}
if queuePlayer == nil {
queuePlayer = AVQueuePlayer(items: lofiSongs)
} else {
queuePlayer?.removeAllItems()
lofiSongs.forEach { queuePlayer?.insert($0, after: nil) }
}
queuePlayer?.seek(to: .zero) // In case we added items back in
queuePlayer?.play()
}
And this works great.
I can also make the lock screen controls appear and use the play pause button like this:
private static func setRemoteControlActions() {
let commandCenter = MPRemoteCommandCenter.shared()
// Add handler for Play Command
commandCenter.playCommand.addTarget { [self] event in
queuePlayer?.play()
return .success
}
// Add handler for Pause Command
commandCenter.pauseCommand.addTarget { [self] event in
if queuePlayer?.rate == 1.0 {
queuePlayer?.pause()
return .success
}
return .commandFailed
}
}
The problem comes with setting the metadata of the player (name, image, etc).
I know it can be done once by setting MPMediaItemPropertyTitle and MPMediaItemArtwork, but how would I change it when the next track loads?
I'm not sure if my approach works for AVQueueplayer, but for playing live streams with AVPlayer you can "listen" to metadata receiving.
extension ViewController: AVPlayerItemMetadataOutputPushDelegate {
func metadataOutput(_ output: AVPlayerItemMetadataOutput, didOutputTimedMetadataGroups groups: [AVTimedMetadataGroup], from track: AVPlayerItemTrack?) {
//look for metadata in groups
}
}
I added the AVPlayerItemMetadataOutputPushDelegate via an extension to my ViewController.
I also found this post.
I hope this gives you a lead to a solution. As said I'm not sure how this works with AVQueuePlayer.

implementation of NSMetadataQuery along with UIDocuments in swiftUI

I am trying to make a document based app in swiftUI with a custom UI. I want iCloud capabilities in my app. I am trying to use iCloud Document (No cloudKit) way for storing data on iCloud container. I am using UIDocument and it's working. It's storing data to iCloud and I am able to retrieve it back.
Now the thing is when I run the app on two devices (iphone and iPad) and make changes to a file on one device, the changes are not reflecting on the other device while the file or say app is open. I have to close the app and relaunch it to see the changes.
I know I have to implement NSMetadataQuery to achieve this but I am struggling with it. I don't know any objective-C. I have been searching on the internet for a good article but could not find any. Can you please tell how do I implement this feature in my app. I have attach the working code of UIDocument and my Model class.
Thank you in advance !
UIDocument
class NoteDocument: UIDocument {
var notes = [Note]()
override func load(fromContents contents: Any, ofType typeName: String?) throws {
if let contents = contents as? Data {
if let arr = try? PropertyListDecoder().decode([Note].self, from: contents) {
self.notes = arr
return
}
}
//if we get here, there was some kind of problem
throw NSError(domain: "NoDataDomain", code: -1, userInfo: nil)
}
override func contents(forType typeName: String) throws -> Any {
if let data = try? PropertyListEncoder().encode(self.notes) {
return data
}
//if we get here, there was some kind of problem
throw NSError(domain: "NoDataDomain", code: -2, userInfo: nil)
}
}
Model
class Model: ObservableObject {
var document: NoteDocument?
var documentURL: URL?
init() {
let fm = FileManager.default
let driveURL = fm.url(forUbiquityContainerIdentifier: nil)?.appendingPathComponent("Documents")
documentURL = driveURL?.appendingPathComponent("savefile.txt")
document = NoteDocument(fileURL: documentURL!)
}
func loadData(viewModel: ViewModel) {
let fm = FileManager.default
if fm.fileExists(atPath: (documentURL?.path)!) {
document?.open(completionHandler: { (success: Bool) -> Void in
if success {
viewModel.notes = self.document?.notes ?? [Note]()
print("File load successfull")
} else {
print("File load failed")
}
})
} else {
document?.save(to: documentURL!, for: .forCreating, completionHandler: { (success: Bool) -> Void in
if success {
print("File create successfull")
} else {
print("File create failed")
}
})
}
}
func saveData(_ notes: [Note]) {
document!.notes = notes
document?.save(to: documentURL!, for: .forOverwriting, completionHandler: { (success: Bool) -> Void in
if success {
print("File save successfull")
} else {
print("File save failed")
}
})
}
func autoSave(_ notes: [Note]) {
document!.notes = notes
document?.updateChangeCount(.done)
}
}
Note
class Note: Identifiable, Codable {
var id = UUID()
var title = ""
var text = ""
}
This is a complex topic. Apple do provide some sample swift code, the Document-Based App Programming Guide for iOS and iCloud Design Guide.
There is also some good third party guidance: Mastering the iCloud Document Store.
I would recommend reading the above, and then return to the NSMetaDataQuery API. NSMetaDataQuery has an initial gathering phase and a live-update phase. The later phase can remain in operation for the lifetime of your app, allowing you to be notified of new documents in your app's iCloud container.

Can't connect players in GameKit using GKMatchmaker.shared().findMatch

I'm trying to connect two players with each other using GameKit in a very simple game. I want to use GKMatchmaker.shared().findMatch as I don't want to show any GameCenter related view controllers. (to keep it simple)
Problem:
Even though GameKit creates a match after finding two players, an error occurs that prevents either player from sending any message to the others.
Current Situation:
The basic code is as follows (based on the docs described here: https://developer.apple.com/documentation/gamekit/finding_multiple_players_for_a_game)
print("Requesting multiplayer match")
let request = GKMatchRequest()
request.minPlayers = 2
request.maxPlayers = 2
request.recipientResponseHandler = {(player: GKPlayer, respnse: GKInviteRecipientResponse) -> Void in
print("new player about to join")
print(player.alias)
print(respnse)
}
GKMatchmaker.shared().findMatch(for: request, withCompletionHandler: {
(match: GKMatch?, error: Error?) -> Void in
if error != nil {
// Handle the error that occurred finding a match.
print("error during matchmaking")
print(error as Any)
} else if match != nil {
guard let match = match else { return }
print("connected to \(match.players.count) players")
// load the multiplayer data handler
let handler = MultiMatchHandler()
match.delegate = handler
// load the multiplayer service
let service = MultiMatchService(match: match)
service.sendMessageToAll(text: "Hello from the other side")
// finish the match making
GKMatchmaker.shared().finishMatchmaking(for: match)
// Start the game with the players in the match.
self.view?.presentScene(GameScene.newScene(multiplayer: service))
}
})
The output of that is
Requesting multiplayer match
2022-01-05 01:19:16.554959+0100 Grapefruit[38300:10026027] [Match] cannot set connecting state for players: (
"<GKPlayer: 0x282add280>(alias:... gamePlayerID:... teamPlayerID:... name:... status:(null) friendBiDirectional:0 friendPlayedWith:1 friendPlayedNearby:0 acceptedGameInviteFromThisFriend:0 initiatedGameInviteToThisFriend:0 automatchedTogether:1)"
), as there is no inviteDelegate set yet. The state might directly change to Ready when we set the inviteDelegate later and call sendQueuedStatesAndPackets.
2022-01-05 01:19:16.557002+0100 Grapefruit[38300:10026027] [Match] syncPlayers failed to loadPlayersForLegacyIdentifiers: (
"..."
)
connected to 0 players
sending text Hello from the other side failed
Findings:
minPlayers is set to 2. As the completion handler is called this means that at least one more player was found. But the number of players returned in match.players.count is 0
The matcher shows an error saying that cannot set connecting state for players ... as there is no inviteDelegate set yet. I can't find any info about this invite delegate.
Actual Question:
What is an inviteDelegate? Do I really need to implement such (if yes, then how?)? (I don't think so as the docs state that the match only starts after the invites are accepted).
How can I resolve this issue?
here is a working example for you. open on two machines, make sure both are authenticated, press "findMatch()" on both machines (and wait for confirmation), then ping baby ping
i believe the "no inviteDelegate set yet" error doesn't mean the match making necessary failed, and can safely be ignored, as mentioned here
you'll want to implement more of the GKMatchDelegate protocol, but this is a skeleton for demonstration purposes
import SwiftUI
import GameKit
import SpriteKit
class MyGameScene: SKScene, GKMatchDelegate {
override func didMove(to view: SKView) {
self.backgroundColor = .yellow
}
//GKMatchDelegate protocol
func match(_ match: GKMatch, didReceive data: Data, forRecipient recipient: GKPlayer, fromRemotePlayer player: GKPlayer) {
print("\(Self.self) \(#function) -- ping received")
}
}
struct Matchmaker: View {
#State var isAuthenticated:Bool = false
#State var scene = MyGameScene()
#State var match:GKMatch? = nil
var body: some View {
ZStack {
Color.clear
SpriteView(scene: scene)
VStack(alignment: .leading, spacing: 20) {
Text("1) authenticate() \(Image(systemName: isAuthenticated ? "checkmark.icloud" : "xmark.icloud"))")
Button { findMatch() } label: {
Text("2) findMatch() \(Image(systemName: (match != nil) ? "person.fill.checkmark" : "person.fill.xmark"))")
}
Button { ping() } label: {
Text("3) ping()")
}
}
}
.onAppear() {
authenticate()
}
}
func authenticate() {
GKLocalPlayer.local.authenticateHandler = { viewController, error in
if let error = error { print(error) }
isAuthenticated = (error == nil)
}
}
func findMatch() {
guard isAuthenticated else { return }
let request = GKMatchRequest()
request.minPlayers = 2
request.maxPlayers = 2
request.playerAttributes = 0xFFFFFFFF //mask for "i'll match with anyone"
GKMatchmaker.shared().findMatch (for: request) { match, error in
if let error = error { print(error) }
self.match = match
self.match?.delegate = scene
}
}
func ping() {
let players = match?.players ?? [];
let data = Data()
do {
try match?.send(data, to: players, dataMode: .reliable)
} catch {
print("Sending failed")
}
}
}

How to keep AVMIDIPlayer playing?

I'm trying to use Apple's AVMIDIPlayer object for playing a MIDI file. It seems easy enough in Swift, using the following code:
let midiFile:NSURL = NSURL(fileURLWithPath:"/path/to/midifile.mid")
var midiPlayer: AVMIDIPlayer?
do {
try midiPlayer = AVMIDIPlayer(contentsOf: midiFile as URL, soundBankURL: nil)
midiPlayer?.prepareToPlay()
} catch {
print("could not create MIDI player")
}
midiPlayer?.play {
print("finished playing")
}
And it plays for about 0.05 seconds. I presume I need to frame it in some kind of loop. I've tried a simple solution:
while stillGoing {
midiPlayer?.play {
let stillGoing = false
}
}
which works, but ramps up the CPU massively. Is there a better way?
Further to the first comment, I've tried making a class, and while it doesn't flag any errors, it doesn't work either.
class midiPlayer {
var player: AVMIDIPlayer?
func play(file: String) {
let myURL = URL(string: file)
do {
try self.player = AVMIDIPlayer.init(contentsOf: myURL!, soundBankURL: nil)
self.player?.prepareToPlay()
} catch {
print("could not create MIDI player")
}
self.player?.play()
}
func stop() {
self.player?.stop()
}
}
// main
let myPlayer = midiPlayer()
let midiFile = "/path/to/midifile.mid"
myPlayer.play(file: midiFile)
You were close with your loop. You just need to give the CPU time to go off and do other things instead of constantly checking to see if midiPlayer is finished yet. Add a call to usleep() in your loop. This one checks every tenth of a second:
let midiFile:NSURL = NSURL(fileURLWithPath:"/Users/steve/Desktop/Untitled.mid")
var midiPlayer: AVMIDIPlayer?
do {
try midiPlayer = AVMIDIPlayer(contentsOfURL: midiFile, soundBankURL: nil)
midiPlayer?.prepareToPlay()
} catch {
print("could not create MIDI player")
}
var stillGoing = true
while stillGoing {
midiPlayer?.play {
print("finished playing")
stillGoing = false
}
usleep(100000)
}
You need to ensure that the midiPlayer object exists until it's done playing. If the above code is just in a single function, midiPlayer will be destroyed when the function returns because there are no remaining references to it. Typically you would declare midiPlayer as a property of an object, like a subclassed controller.
Combining Brendan and Steve's answers, the key is sleep or usleep and sticking the play method outside the loop to avoid revving the CPU.
player?.play({return})
while player!.isPlaying {
sleep(1) // or usleep(10000)
}
The original stillGoing value works, but there is also an isPlaying method.
.play needs something between its brackets to avoid hanging forever after completion.
Many thanks.