SingletonMusicPlayer playing overitself - swift

I want a midi manager that means it can be called from anywhere...a Singleton instance...and handle music requests from different threads.
This is in the Playground
import PlaygroundSupport
import AudioToolbox
class MusicPlayerManager {
static let sharedInstance = MusicPlayerManager()
private init() {}
var musicPlayer : MusicPlayer? = nil
var sequence : MusicSequence? = nil
var track : MusicTrack? = nil
var time = MusicTimeStamp(1.0)
var player: OSStatus? = nil
var musicTrack: OSStatus? = nil
func playNotes(notes: [UInt8]) {
_ = NewMusicSequence(&sequence)
player = NewMusicPlayer(&musicPlayer)
player = MusicPlayerSetSequence(musicPlayer!, sequence)
player = MusicPlayerStart(musicPlayer!)
musicTrack = MusicSequenceNewTrack(sequence!, &track)
for index:Int in 0...6 {
var note = MIDINoteMessage(channel: 0,
note: notes[index],
velocity: 64,
releaseVelocity: 0,
duration: 1.0)
guard let track = track else {fatalError()}
musicTrack = MusicTrackNewMIDINoteEvent(track, time, &note)
time += 1
}
player = MusicPlayerSetSequence(musicPlayer!, sequence)
player = MusicPlayerStart(musicPlayer!)
}
}
var notes: [UInt8] = [71,69,62,72,71,69,67]
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
MusicPlayerManager.sharedInstance.playNotes(notes: notes)
}
DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
MusicPlayerManager.sharedInstance.playNotes(notes: notes)
}
PlaygroundPage.current.needsIndefiniteExecution = true
Unfortunately the music often doesn't play at all, or plays over itself from each instance.
At worst it should play the first instance of the music; why doesn't it nicely do that?

I think you ran into a form of readers-writers problem here by attempting to simultaneously manipulate instance variables on your singleton from different threads. I don't know what your desired music player behavior is if it's being called from multiple threads at once, but for me one easy improvement would be to give your MusicPlayerManager singleton it's own DispatchQueue, where multiple overlapping requests can be handled sequentially.
You could define one like this:
let concurrentQueue = DispatchQueue(label: "com.stackoverflow.gamma", attributes: .concurrent)
And then wrap the code inside the playNotes function like this:
func playNotes(notes: [UInt8]) {
self.concurrentQueue.async(flags: .barrier) {
...
}
}

Related

SwiftUI: asynchronously making a calculating and show its result in a sheet

I'm making a CrossFit app that has, as one of the features, a stopwatch or timer that the user will start and stop, and when it's stopped, it'll log the exercise to Health Kit, it will calculate the approximate calories based on the exercise intensity and time to complete the exercise, and will finally show a sheet telling the user the number of calories consumed and how much time it took.
The view that contains the stopwatch and the buttons is the following:
struct FooterTimerView: View {
#State private var completedSheetPresented = false
var workout: Workout? = nil
var subworkout: Subworkout? = nil
#Environment(\.managedObjectContext) private var viewContext
#EnvironmentObject var healthViewModel: HealthViewModel
fileprivate func getConsumedCaloriesAndShowSheet(_ completable: Completable, timeInMinutes minutes: Int) async -> Double {
return await healthViewModel.logCompletable(completable, timeInMinutes: minutes, inContext: viewContext) ?? 0.0
}
var body: some View {
var loggedTime = ("", "", "")
var consumedCalories = 0.0
return TimerView { (timeString: String) in
loggedTime = converTimeStringToTimeInterval(timeString)
if let minutes = Int(loggedTime.0), let seconds = Int(loggedTime.1), let miliseconds = Int(loggedTime.2) {
let totalMinutes = minutes + seconds / 60 + (miliseconds / (60 * 1000))
if let workout = workout {
Task {
consumedCalories = await getConsumedCaloriesAndShowSheet(workout, timeInMinutes: totalMinutes)
completedSheetPresented = true
}
} else if let subworkout = subworkout {
Task {
consumedCalories = await getConsumedCaloriesAndShowSheet(subworkout, timeInMinutes: totalMinutes)
completedSheetPresented = true
}
}
}
}.sheet(isPresented: $completedSheetPresented) {
CompletedWorkoutSheet(workoutTime: loggedTime, workoutCalories: consumedCalories)
}
}
private func converTimeStringToTimeInterval(_ timeString: String) -> (String, String, String) {
let timeString = "59:56:23"
let components = timeString.components(separatedBy: ":")
let millis = components[2]
let seconds = components[1]
let minutes = components[0]
return (minutes, seconds, millis)
}
}
To notice in the code, the two variables that the sheet will need (loggedTime and consumedCalories) are defined in the body of the view, so the sheet can use their values, and I'm not sure if there's a better way to do this.
Also, my view TimerView which is created in the code above has the following code:
struct TimerView: View {
let stopWatchFrameHeight = CGFloat(70)
let stopWatchFontSize = CGFloat(70)
#StateObject var stopWatch = StopWatch()
let onTimerFinished: (String) -> Void
init(onTimerFinishedAction: #escaping (String) -> Void) {
self.onTimerFinished = onTimerFinishedAction
}
so when it is called in the FooterTimerView, the lambda passed to it is a function to be executed when the timer is finished - which is when the consumed calories can be calculated based on the time spent exercising.
The problem I'm facing is that in the two assignments to the consumedCalories variable, I get the error Mutation of captured var 'consumedCalories' in concurrently-executing code and I'm not sure how to fix the code so that:
The amount of calories is calculated first in an async way.
Once the 1. is finished, the sheet is shown, and it takes the consumedCalories value to it is shown to the user.
I've been trying to fix it for a couple of days and as soon as I think I'm heading in the right direction and I start fixing it, I always end up at a dead end.
Thanks a lot in advance!

SharingMusicPlayer.swift:12:79: Cannot convert value of type 'AVAudioPlayer.Type' to expected argument type 'AVAudioPlayer'

This is what I have in my singleton:
import AVFoundation
import Foundation
class SharingMusicPlayer {
static let sharingMusicPlayer = SharingMusicPlayer(backgroundMusicPlayer: AVAudioPlayer)
let backgroundMusicPlayer : AVAudioPlayer
private init(backgroundMusicPlayer: AVAudioPlayer) {
self.backgroundMusicPlayer = backgroundMusicPlayer
}
func playMusic() {
// open and play an mp3 file...
}
}
But I am getting this error:
SharingMusicPlayer.swift:12:79: Cannot convert value of type 'AVAudioPlayer.Type' to expected argument type 'AVAudioPlayer'
Here is another post I have investigated but the solution does not appear to apply here:
"Cannot convert value of type 'AVAudioPlayer.Type' to expected argument type"
BTW, I based my singleton pattern on this article:
https://cocoacasts.com/what-is-a-singleton-and-how-to-create-one-in-swift
Does anyone have any suggestions?
UPDATE:
Please note that I realize that I can play a sound in my app using this code:
run(SKAction.playSoundFileNamed("MySound.mp3", waitForCompletion: false))
But I want to play background music in this case so the above doesn't work.
You are passing it the type AVAudioPlayer when you need to pass an instance of AVAudioPlayer. This should work I guess:
Change
static let sharingMusicPlayer = SharingMusicPlayer(backgroundMusicPlayer: AVAudioPlayer)
to
static let sharingMusicPlayer
= SharingMusicPlayer(backgroundMusicPlayer: AVAudioPlayer())
Update
This is the full code that compiles at my end
class SharingMusicPlayer {
static let sharingMusicPlayer
= SharingMusicPlayer(backgroundMusicPlayer: AVAudioPlayer())
let backgroundMusicPlayer : AVAudioPlayer
private init(backgroundMusicPlayer: AVAudioPlayer) {
self.backgroundMusicPlayer = backgroundMusicPlayer
}
func playMusic() {
// open and play an mp3 file...
}
}
This is what I came up with, though I am still not sure if this is the right way to do a singleton in swift:
class SharingMusicPlayer {
static var sharingMusicPlayer = SharingMusicPlayer()
var backgroundMusicPlayer : AVAudioPlayer?
private init() {
}
func playBackgroundMusic(filename: String) {
let url = Bundle.main.url(
forResource: filename, withExtension: nil)
if (url == nil) {
print("Could not find file: \(filename)")
return
}
do {
try self.backgroundMusicPlayer = AVAudioPlayer(contentsOf: url!)
} catch {
print("Could not create audio player")
}
backgroundMusicPlayer!.numberOfLoops = -1
backgroundMusicPlayer!.prepareToPlay()
backgroundMusicPlayer!.play()
}
func stopBackgroundMusic(filename: String) {
self.backgroundMusicPlayer!.stop()
}
}
Then this is how I would call the functions to start and stop the music:
override func touchesBegan(_ touches: Set<UITouch>,
with event: UIEvent?) {
/* Called when a touch begins */
for touch: AnyObject in touches {
//1
let location = touch.location(in:self)
//2
let theNode = self.atPoint(location)
//...
if theNode.name == playMusicButton!.name {
print("The \(playMusicButton!.name!) is touched ")
SharingMusicPlayer.sharingMusicPlayer.playBackgroundMusic(
filename: "BeethovenPianoSonataNr15InDmajorOp28Pastoral.mp3")
}
if theNode.name == stopMusicButton!.name {
print("The \(stopMusicButton!.name!) is touched ")
SharingMusicPlayer.sharingMusicPlayer.stopBackgroundMusic()
}
}
Please note that the above buttons are in SpriteKit so they are different than the usual UIButton objects in Main.storyboard.

Executing text-to-speech in order

I want to synthesize text. I have an array of sentences and array of pauses, that I wish between these sentences.
What was the thought
Synthesize -> start the timer, timer fires after provided time -> Synthesize -> start the timer -> Synt...
By chance, I've noticed that timer fires the lesser time first, instead of executing and setting up timers in sequence. The loop doesn't wait till synthesizer finished to pronounce, it continues to run.
How to work out that synthesizer pronounces sentences with provided pauses, and in order?
import SwiftUI
struct KingsSpeechView: View {
#ObservedObject var speaker = Speaker()
#State private var subtitles = ""
#State private var currentStepIndex = 0
let kingsSpeech = [
"Hello. Let's start the Game! Let the hunger Games Begin...Whoa-Whoa. Here're are the rules on the screen.",
"Okey, now that you know the rules, chill out. Let's play another game.",
"You say Hi, I say Ho.",
"Hooo",
"Hooo"
]
var pauses = [0.0, 20.0, 90.0, 40.0, 40.0]
// try to change into this
// var pauses = [0.0, 20.0, 10.0, 5.0, 5.0]
// the sequence of execution is completely different
// the ones that has less value, will execute first
// While I expected it to execute in order it is in array, instead it runs as it runs (wants)
// (or maybe it's the case it's just one timer for all)
// How to prevent loop from continuing to new iteration until the speech is not pronounced?
var body: some View {
VStack {
Text(subtitles)
.padding(.bottom, 50)
.padding(.horizontal, 20)
Button("Play") {
playSound()
}
}
}
func playSound() {
for step in 0..<kingsSpeech.count {
let timer = Timer.scheduledTimer(withTimeInterval: pauses[step], repeats: false) { timer in
subtitles = kingsSpeech[step]
speaker.speak("\(kingsSpeech[step])")
print("I am out")
currentStepIndex += 1
// I've tried to stop a loop from moving on, before the speech had finished to pronounce
// with some sort of a condition maybe; by index or by identifying if the synthesizer is speaking
// but it even turned out that timer executes completely different, look in time arrays above
// while speaker.semaphoreIndex == step {
// print("still waiting")
// }
// while speaker.synth.isSpeaking {
//
// }
}
}
}
}
...
import AVFoundation
import Combine
class Speaker: NSObject, ObservableObject, AVSpeechSynthesizerDelegate {
let synth = AVSpeechSynthesizer()
// started to try something with simophore, but didn't understand how to implement it
var semaphore = DispatchSemaphore(value: 0)
var semaphoreIndex = 0
override init() {
super.init()
synth.delegate = self
}
func speak(_ string: String) {
let utterance = AVSpeechUtterance(string: string)
utterance.voice = AVSpeechSynthesisVoice(language: "en-GB")
utterance.rate = 0.4
synth.speak(utterance)
}
}
extension Speaker {
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance) {
print("all done")
semaphore.signal()
semaphoreIndex += 1
}
}
Just speak an utterance, receive the delegate method, and in that method wait the desired interval and go on to the next utterance and interval.
Here's a complete example. It uses a Cocoa project, not SwiftUI, but you can easily adapt it.
import UIKit
import AVFoundation
func delay(_ delay:Double, closure:#escaping ()->()) {
let when = DispatchTime.now() + delay
DispatchQueue.main.asyncAfter(deadline: when, execute: closure)
}
class Speaker : NSObject, AVSpeechSynthesizerDelegate {
var synth : AVSpeechSynthesizer!
var sentences = [String]()
var intervals = [Double]()
func start(_ sentences: [String], _ intervals: [Double]) {
self.sentences = sentences
self.intervals = intervals
self.synth = AVSpeechSynthesizer()
synth.delegate = self
self.sayOne()
}
func sayOne() {
if let sentence = sentences.first {
sentences.removeFirst()
let utter = AVSpeechUtterance(string: sentence)
self.synth.speak(utter)
}
}
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance) {
if let interval = intervals.first {
intervals.removeFirst()
delay(interval) {
self.sayOne()
}
}
}
}
class ViewController: UIViewController {
let speaker = Speaker()
override func viewDidLoad() {
super.viewDidLoad()
let sentences = [
"I will speak again in one second",
"I will speak again in five seconds",
"I will speak again in one second",
"Done"]
let intervals = [1.0, 5.0, 1.0]
self.speaker.start(sentences, intervals)
}
}
Trying to answer the question that I asked in comments to solution:
For now, it can play/ pause
TODO: Now I have to discover how to jump backward/ forward between sentences. So, for this I firstly need to stop the current speech task. speaker.synth.stopSpeaking(at: .word)
Then, I maybe should have some index tracking what's the current stage is. Then, when I stopped the task, I remember the index. And I can go backward/ forward. Now start from index-1 or index+1 place, rather than from the beginning.
#State private var isPlaying = false
...
// play button
Button(action: {
if isPlaying {
isPlaying.toggle()
speaker.synth.pauseSpeaking(at: .word)
} else {
isPlaying.toggle()
// continue playing here if it was paused before, else ignite speech utterance
if speaker.synth.isPaused {
speaker.synth.continueSpeaking()
} else {
speaker.speak()
}
}
}, label: {
Image(systemName: (isPlaying ? "pause.fill" : "play.fill"))
.resizable()
.scaledToFit()
.frame(width: 50, height: 50)
})

How to properly connect NSColorPanel to a server to avoid overload

I'm using NSColorPanel to change the color of a view.
The color of this view is also saved in a database (Firestore).
import AppKit
class ColorPanel {
static var shared = ColorPanel()
private var stage: DB.Stage.Document? = nil
private let cp = NSColorPanel.shared
init() {
cp.setTarget(self)
cp.setAction(#selector(colorDidChange(sender:)))
cp.isContinuous = false
}
func show(stage: DB.Stage.Document) {
self.stage = stage
cp.makeKeyAndOrderFront(nil)
}
#objc func colorDidChange(sender: NSColorPanel) {
guard let stage = stage else { return }
stage.data?.color.red = Double(sender.color.redComponent)
stage.data?.color.green = Double(sender.color.greenComponent)
stage.data?.color.blue = Double(sender.color.blueComponent)
stage.update()
}
}
The problem is that I would like to set isContinuos true in order to see my view changing color real-time, but is sending too much updates to the server, so I have been forced to set it false.
There is a way to solve this? I just need to do an update when I finish the drag but I don't know how.
p.s. To call the ColorPanel in my SwiftUI view I do:
ColorPanel.shared.show(stage: stage)
Please try an approach I would use. Disclaimer: not tested due to absent Firestore setup
import Combine
class ColorPanel {
static var shared = ColorPanel()
private var stage: DB.Stage.Document? = nil
private let cp = NSColorPanel.shared
private var subscriber: AnyCancellable?
private let publisher =
PassthroughSubject<NSColor, Never>()
.throttle(for: 10, scheduler: RunLoop.main, latest: true)
init() {
cp.setTarget(self)
cp.setAction(#selector(colorDidChange(sender:)))
cp.isContinuous = true
}
func show(stage: DB.Stage.Document) {
self.stage = stage
self.subscriber = nil
if stage != nil {
self.subscriber = self.publisher
.sink { _ in
self.stage.update() // << be called once per 10 seconds
}
}
cp.makeKeyAndOrderFront(nil)
}
#objc func colorDidChange(sender: NSColorPanel) {
guard let stage = stage else { return }
stage.data?.color.red = Double(sender.color.redComponent)
stage.data?.color.green = Double(sender.color.greenComponent)
stage.data?.color.blue = Double(sender.color.blueComponent)
self.publisher.upstream.send(sender.color)
}
}

Saving High Score Data with Swift

I am trying to save a high score using Swift for my SpriteKit game. There are several good examples on StackOverflow, one of which I got to work temporarily, but could not function properly in the Swift file where all of my nodes (and actual game) is located.
*Most of the following code is from a stack overflow answer.
This code I put in a separate file called "HighScore":
import Foundation
class HighScore: NSObject {
var highScore: Int = 0
func encodeWithCoder(aCoder: NSCoder!) {
aCoder.encodeInteger(highScore, forKey: "highScore")
}
init(coder aDecoder: NSCoder!) {
highScore = aDecoder.decodeIntegerForKey("highScore")
}
override init() {
}
}
class SaveHighScore:NSObject {
var documentDirectories:NSArray = []
var documentDirectory:String = ""
var path:String = ""
func ArchiveHighScore(#highScore: HighScore) {
documentDirectories = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)
documentDirectory = documentDirectories.objectAtIndex(0) as String
path = documentDirectory.stringByAppendingPathComponent("highScore.archive")
if NSKeyedArchiver.archiveRootObject(highScore, toFile: path) {
println("Success writing to file!")
} else {
println("Unable to write to file!")
}
}
func RetrieveHighScore() -> NSObject {
var dataToRetrieve = HighScore()
documentDirectories = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)
documentDirectory = documentDirectories.objectAtIndex(0) as String
path = documentDirectory.stringByAppendingPathComponent("highScore.archive")
if let dataToRetrieve2 = NSKeyedUnarchiver.unarchiveObjectWithFile(path) as? HighScore {
dataToRetrieve = dataToRetrieve2
}
return(dataToRetrieve)
}
}
In the scene where I actually want to get an input and output for the highscore I have:
var Score = HighScore()
override func viewDidLoad() {
super.viewDidLoad()
Score.highScore = 100
SaveHighScore().ArchiveHighScore(highScore: Score)
var retrievedHighScore = SaveHighScore().RetrieveHighScore() as HighScore
println(retrievedHighScore.highScore)
}
So when a particular node passes a "block" the high score should increment accordingly, and then save the number (as long as it is higher than the current highscore.)
func blockRunner() {
Score.highScore = 0
SaveHighScore().ArchiveHighScore(highScore: Score)
var retrievedHighScore = SaveHighScore().RetrieveHighScore() as! HighScore
println(retrievedHighScore.highScore)
for(block, blockStatus) in blockStatuses {
var thisBlock = self.childNodeWithName(block)!
if blockStatus.shouldRunBlock() {
blockStatus.timeGapForNextRun = random()
blockStatus.currentInterval = 0
blockStatus.isRunning = true
}
if blockStatus.isRunning {
if thisBlock.position.x > blockMaxX {
thisBlock.position.x -= CGFloat(groundspeed)
}
else{
thisBlock.position.x = self.origBlockPositionX
blockStatus.isRunning = false
retrievedHighScore.highScore++
if ((retrievedHighScore.highScore % 5) == 0) {
groundspeed++
}
self.scoreText.text = String(retrievedHighScore.highScore++)
}
}else{
blockStatus.currentInterval++
}
}
}
For some reason, it will only increment to 1 and then just display 1 in scoreText, even if it has passed more than one block. If I just declare a normal variable and subtitute it in for retrievedHighScore.highScore++, everything works fine. When I use retrievedHighScore.highScore, it only increments to one and just displays 1 in scoreText, strangely, the 1 isn't even saved.
I really recommend using NSUserDefaults in this situation to persist your high score. I also recommend not creating a highscores object for the sake of simply having an Integer variable. You're creating alot of unnecessary overhead by utilizing a class to model a simple Integer.
All that code for archiving highscore can be simplified to 1 line: NSUserDefaults.standardUserDefaults().setInteger(highScore, forKey: "Highscore")
When you need to overwrite the highscore (when a new highscore is to replace the old one) you can simply overwrite the old highscore by calling the above line of code again.
You save alot of work, and your code will perform more efficiently. Using a class to store a single value type in your situation is a terrible choice. Crusty will be mad...