diff --git a/AutomneAxioms.swift b/AutomneAxioms.swift index 0f882d3..ea12320 100644 --- a/AutomneAxioms.swift +++ b/AutomneAxioms.swift @@ -17,101 +17,126 @@ class AutomneAxioms{ public static let SCTailQueue = "?client_id=" public static let emojis = ["📡", "🎛", "🎚", "🎙", "📻", "📀", "💿", "💽", "🌌", "🎹", "🎧", "🎤", "🍂", "🍁", "🦗", "🌤", "🦊", "🎃", "🌲", "🥘", "🚲", "🚉", "🏕", "🛤", "🗺"] - public static var messages = ["You are loved.", - "You are not alone.", - "Я не помню, как я оказался в лесу", - "These are my friends", - "Leaves are always yellow in Golovkovo", - "Smash the government", - "You always have a chance.", - "Don't forget who you are", - "Nature is fascinating", - "This is from Matilda", - "We watched the end of the century, compressed on a tiny screen", - "At every occasion, i'll be ready for the funeral", - "To see age in a flower, the dawns are speeding up", - "Life's alright in devil town", - "Увидимся в Лапшичной", - "В Хижине на Холме чай пахнет ёлками", - "Не бросайте своих собак", - "Ветер воет в форточках Полустанка", - "Why would dogs avoid these hills?", - "@phvkha is the best photographer i've ever known", - "You're welcome anytime in my dreams", - "Apocalypse", - "Life is a drink, and love's a drug", - "I never meant to cause you trouble", - "Истории Петербурга – одни из лучших историй", - "You're standing out in the rain tonight", - "I love my grandparents", - "TIL I DIE DIE DIE", - "Nothing's terrible with friends around", - "Навсегда юность, навсегда смерть", - "Au sе́maphore ton nom rе́sonne", - "I used to fit in your arms, like a book in a shelf", - "Now I sit on the floor, telling jokes to myself", - "Я снова проспал и проснулся в обед", - "Прости, я забыл, что тебя нет", - "I won't hurt you, I won't hurt you", - "I'll tell you, Fenn, i'll tell you, when", - "It's now", - "I love you.", - "You're always welcome to Pokrovka Dacha", - "You are valid", - "Все в порядке, все пройдет", - "Утро которым мы умрем", - "You, you feel like Oxford blood", - "Where goes that path through the woods?", - "There goes our love again", - "Я встречусь с тобой осенью восьмого класса", - "Born in Possum Springs", - "R.I.P. Grandma", - "R.I.P. Alec", - "Ew a furry", - "Caring is the coolest thing I've seen anyone do.", - "It was a nice holiday without you", - "And it's called jazz", - "It's the colours you have, no need to be sad", - "Are you ready for the Longest Night?", - "And I think we'd survive in the wild", - "Слушать старые пластинки", - "I only have one thing in my head", - "Пойдем фоткаться на пленку", - "Гоу в Балчуг после уроков", - "Я такой один, мне не нужно притворяться", - "#038", - "26.04.2019 – ∞", - "Please, if you hear me, go away", - "Все получится", - "И тебе приснится целый мир без меня", - "Я заберу тебя танцевать", - "Знаешь, я так соскучился", - "Lavender is always running through my blood", - "Trapped in a club", - "All we had to do was touch", - "Spies hide out in every corner", - "Tears falling down at the party", - "Saddest little baby in the room", - "Еще одну бессонную ночь я посвящаю тебе", - "Поды для джула вкуснее всего в Люберцах", - "This couldn't happen again", - "I'd rather dissolve than have you ignore me", - "I miss the Weird Autumn", - "Сountryside sceneries hardly change", - "the holes of my sweater", - "blood like wine", - "Вишенку так я и не достал", - "Хочу Цезарь из Царского Села", - "В ЦДМ есть Шоколадница", - "Я смешаю коньяк и Байкал", - "Two oceans in between us", - "I left you at the farm", - "We had a good time, didn't we?", - "Drink at the casino all night", - "sunsets i wanna hear your voice", - "God Bless My Socially Retarded Friends", - "Лето тупо класс", - "хочу питсы"] + public static var messages = [ + "You are loved.", + "You are not alone.", + "Я не помню, как я оказался в лесу", + "These are my friends", + "Leaves are always yellow in Golovkovo", + "Smash the government", + "You always have a chance.", + "Don't forget who you are", + "Nature is fascinating", + "This is from Matilda", + "We watched the end of the century, compressed on a tiny screen", + "At every occasion, i'll be ready for the funeral", + "To see age in a flower, the dawns are speeding up", + "Life's alright in devil town", + "Увидимся в Лапшичной", + "В Хижине на Холме чай пахнет ёлками", + "Не бросайте своих собак", + "Ветер воет в форточках Полустанка", + "Why would dogs avoid these hills?", + "@phvkha is the best photographer i've ever known", + "You're welcome anytime in my dreams", + "Apocalypse", + "Life is a drink, and love's a drug", + "I never meant to cause you trouble", + "Истории Петербурга – одни из лучших историй", + "You're standing out in the rain tonight", + "I love my grandparents", + "TIL I DIE DIE DIE", + "Nothing's terrible with friends around", + "Навсегда юность, навсегда смерть", + "Au sе́maphore ton nom rе́sonne", + "I used to fit in your arms, like a book in a shelf", + "Now I sit on the floor, telling jokes to myself", + "Я снова проспал и проснулся в обед", + "Прости, я забыл, что тебя нет", + "I won't hurt you, I won't hurt you", + "I'll tell you, Fenn, i'll tell you, when", + "It's now", + "I love you.", + "You're always welcome to Pokrovka Dacha", + "You are valid", + "Все в порядке, все пройдет", + "Утро которым мы умрем", + "You, you feel like Oxford blood", + "Where goes that path through the woods?", + "There goes our love again", + "Я встречусь с тобой осенью восьмого класса", + "Born in Possum Springs", + "R.I.P. Grandma", + "R.I.P. Alec", + "Ew a furry", + "Caring is the coolest thing I've seen anyone do.", + "It was a nice holiday without you", + "And it's called jazz", + "It's the colours you have, no need to be sad", + "Are you ready for the Longest Night?", + "And I think we'd survive in the wild", + "Слушать старые пластинки", + "I only have one thing in my head", + "Пойдем фоткаться на пленку", + "Гоу в Балчуг после уроков", + "Я такой один, мне не нужно притворяться", + "#038", + "26.04.2019 – ∞", + "Please, if you hear me, go away", + "Все получится", + "И тебе приснится целый мир без меня", + "Я заберу тебя танцевать", + "Знаешь, я так соскучился", + "Lavender is always running through my blood", + "Trapped in a club", + "All we had to do was touch", + "Spies hide out in every corner", + "Tears falling down at the party", + "Saddest little baby in the room", + "Еще одну бессонную ночь я посвящаю тебе", + "Поды для джула вкуснее всего в Люберцах", + "This couldn't happen again", + "I'd rather dissolve than have you ignore me", + "I miss the Weird Autumn", + "Сountryside sceneries hardly change", + "the holes of my sweater", + "blood like wine", + "Вишенку так я и не достал", + "Хочу Цезарь из Царского Села", + "В ЦДМ есть Шоколадница", + "Я смешаю коньяк и Байкал", + "Two oceans in between us", + "I left you at the farm", + "We had a good time, didn't we?", + "Drink at the casino all night", + "sunsets i wanna hear your voice", + "God Bless My Socially Retarded Friends", + "Лето тупо класс", + "хочу питсы", + "Застрять, как зубная нить между собачьими клыками", + "To the Neon District", + "Да ну его, это чистое сердце. Шоколад, по моему, гораздо лучше", + "Старость – самая большая неожиданность в жизни", + "We missed you a lot", + "Welcome back", + "Lester hugs you for using Automne", + "Радио Осень – всегда в прямом эфире", + "В мире много бессмыслицы", + "Что может быть бессмысленнее, чем проснуться утром одному в номере интим-гостиницы?", + "Апрель – слишком грустная пора, чтобы проводить ее в одиночестве", + "Одни, скинув тяжелые куртки, беседовали на солнышке", + "Другие играли в кэтч-бол", + "Третьи любили", + "А я был полностью одинок", + "Вдобавок ко всему, я был влюблен", + "И любовь эта привела меня в очень непростое место", + "Неудовлетворенное страстное желание отрочества", + "Мир просторен", + "Общаясь с ним, иногда ловишь себя на мысли, что ходишь по кругу", + "Кассетная камера всегда под рукой", + "Magic happens when cassettes are being recorded", + "Hold on, let me find my walkman" + ] public static let trackNarratives = [ ("Now playing", true), @@ -128,11 +153,6 @@ class AutomneAxioms{ ("that's the $", false), ("is up to your ears", false) ] - public static var specialNarratives = [ - "Welcome back", - "Never settle", - "We missed you a lot" - ] public static let specialTimeNarratives = [ "morning": [ "Good morning", @@ -144,7 +164,7 @@ class AutomneAxioms{ "Вот оно, утро, с которого надо начинать день" ], "day": [ - "This day is great, is not it?", + "This day is great, isn't it?", "Autumn brings music to your day", "Добрый денёчек", "Пусть этот день будет лучше, чем вчерашний", @@ -178,6 +198,13 @@ class AutomneAxioms{ "Засыпай, самая лучшая боль" ] ] + public static let specialWelcomeNarratives = [ + "Welcome", + "Welcome back", + "Glad to see you", + "Hello my friend", + "Seeing you is always a pleasure" + ] public static func uniq(source: S) -> [T] where S.Iterator.Element == T { var buffer = [T]() diff --git a/CoreScripts.swift b/CoreScripts.swift index 2c456bb..4ca969b 100644 --- a/CoreScripts.swift +++ b/CoreScripts.swift @@ -57,4 +57,11 @@ class AutomneCore { NSUserNotificationCenter.default.deliver(notification) } } + public static func displayAlert(title: String, message: String){ + let alert = NSAlert() + alert.messageText = title + alert.informativeText = message + alert.addButton(withTitle: "OK") + alert.runModal() + } } diff --git a/SFX Processor.swift b/SFX Processor.swift index ab69ad4..e3900f1 100644 --- a/SFX Processor.swift +++ b/SFX Processor.swift @@ -8,10 +8,14 @@ import Foundation import AVFoundation +import Cocoa class SFX { private static var player = AVAudioPlayer() private static let synth = AVSpeechSynthesizer() + public static var voiceIdentifier: String? = nil + private static let preferredVoices = ["ava.premium", "samantha.premium", "daniel", "alex"] + public enum Effects: String { case powerOn = "PowOn" case buttonClick = "Button" @@ -30,44 +34,71 @@ class SFX { } } - public static func shutUp(){ + public static func playSFX(sfx: String){ + NSSound(named: sfx)?.play() + } + + public static func shutUp(speaker: Bool = false){ player.stop() - synth.stopSpeaking(at: .immediate) + if speaker { synth.stopSpeaking(at: .immediate) } + } + + public static func testVoices() { + print(AVSpeechSynthesisVoice.speechVoices()) } public static func speak(say: String, lang: String) { let utterance = AVSpeechUtterance(string: say) switch lang { - case "en-US": - utterance.voice = AVSpeechSynthesisVoice(identifier: "com.apple.speech.synthesis.voice.samantha") - default: - utterance.voice = AVSpeechSynthesisVoice(language: lang) + case "en": + if voiceIdentifier == nil { + for s in preferredVoices{ + let d = "com.apple.speech.synthesis.voice." + s + if AVSpeechSynthesisVoice(identifier: d) != nil { + voiceIdentifier = d + break + } + } + if voiceIdentifier == nil { + ViewController.defaults.set(0, forKey: "narrator") + AutomneCore.displayAlert(title: "No voices found", message: "Automne was unable to find any voices downloaded on this machine. Narrator will be switched off. Refer to manual in order to download a voice") + return + } + } + utterance.voice = AVSpeechSynthesisVoice(identifier: voiceIdentifier!) + default: + utterance.voice = AVSpeechSynthesisVoice(language: lang) } utterance.rate = 0.4 - utterance.volume = 1 - utterance.preUtteranceDelay = 1 + utterance.volume = 0.7 + utterance.postUtteranceDelay = 0.7 + playSFX(sfx: "Blow") synth.speak(utterance) } + public static func speakWelcome(){ + speak(say: AutomneAxioms.specialWelcomeNarratives.randomElement()!, lang: "en") + } + public static func composeAndSpeak(track: String, artist: String) -> Bool { if synth.isSpeaking { return false } - let a = Int.random(in: 1...7) + let a = Int.random(in: 1...8) if a == 1 || a == 2 { let s = AutomneAxioms.trackNarratives.randomElement() let d = a == 1 ? track : artist let f = s!.0.replacingOccurrences(of: "$", with: a == 1 ? "track" : "artist") let lang: String - if d.isLatin { lang = "en-US" } + if d.isLatin { lang = "en" } else if d.isCyrillic { lang = "ru-RU" } - else { return false} - if s!.1 { speak(say: f, lang: "en-US") } + else { return false } + if s!.1 { speak(say: f, lang: "en") } speak(say: d, lang: lang) - if !s!.1 { speak(say: f, lang: "en-US") } + if !s!.1 { speak(say: f, lang: "en") } } else if a == 3 || a == 4 { let s: String if a == 3 { - s = AutomneAxioms.specialNarratives.randomElement()! + s = AutomneAxioms.messages.randomElement()! } else { let date = Date() let calendar = Calendar.current @@ -88,10 +119,14 @@ class SFX { s = AutomneAxioms.specialTimeNarratives[d]!.randomElement()! } if s.isLatin { - speak(say: s, lang: "en-US") - } else { + speak(say: s, lang: "en") + } else if s.isCyrillic { speak(say: s, lang: "ru-RU") + } else { + return false } + } else { + return false } return true } diff --git a/ViewController.swift b/ViewController.swift index c9a7661..352da90 100644 --- a/ViewController.swift +++ b/ViewController.swift @@ -9,6 +9,7 @@ import Cocoa import Alamofire import AVFoundation +import MediaPlayer class View: NSView { override func performKeyEquivalent(with event: NSEvent) -> Bool { @@ -79,7 +80,7 @@ class ViewController: NSViewController{ let fr = ViewController.selectedFrequency! if fr.isStream! { self.startStream(frequency: fr) - }else{ + } else { self.retrieveTracks(frequency: fr) } } @@ -227,6 +228,8 @@ class ViewController: NSViewController{ //********************************************************************* let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String static let defaults = UserDefaults.standard + static let controlCenter = MPRemoteCommandCenter.shared() + static let controlCenterInfo = MPNowPlayingInfoCenter.default() let fetchLogo = "### ### ### ### # #\n# # # # # #\n## ### # # ###\n# # # # # #\n# ### # ### # #" static let states = [MainDisplayState.frequency, MainDisplayState.song, MainDisplayState.artist, MainDisplayState.time] static let streamStates = [MainDisplayState.frequency, MainDisplayState.song, MainDisplayState.time] @@ -236,6 +239,51 @@ class ViewController: NSViewController{ //********************************************************************* override func viewDidAppear() { super.viewDidAppear() + ViewController.controlCenter.playCommand.addTarget {_ in + if ViewController.playbackControllerState == .paused { + self.resume() + return .success + } else { + return .commandFailed + } + } + ViewController.controlCenter.pauseCommand.addTarget {_ in + if ViewController.playbackControllerState == .playing { + self.pause() + return .success + } else { + return .commandFailed + } + } + ViewController.controlCenter.nextTrackCommand.addTarget {_ in + if ViewController.systemStatus == .active && (ViewController.setFrequency == nil || !(ViewController.setFrequency?.isStream ?? true)){ + self.advance() + return .success + } else { + return .commandFailed + } + } + ViewController.controlCenter.previousTrackCommand.addTarget {_ in + if ViewController.systemStatus == .active && (ViewController.setFrequency == nil || !(ViewController.setFrequency?.isStream ?? true)){ + self.reverse() + return .success + } else { + return .commandFailed + } + } + ViewController.controlCenter.seekForwardCommand.addTarget {_ in + return .commandFailed + } + ViewController.controlCenter.seekBackwardCommand.addTarget {_ in + return .commandFailed + } + ViewController.controlCenter.changePlaybackPositionCommand.addTarget {_ in + if ViewController.controlCenterInfo.nowPlayingInfo != nil { + ViewController.controlCenterInfo.nowPlayingInfo!["MPNowPlayingInfoPropertyElapsedPlaybackTime"] = ViewController.player.currentTime().seconds + } + return .commandFailed + } + // SFX.testVoices() // Uncomment to reset user settings // // ViewController.defaults.set(0, forKey: "artwork") @@ -373,9 +421,6 @@ class ViewController: NSViewController{ tprint(AutomneKeys.dedication, raw: true, noWipe: true) } } -// else if event.keyCode == 45 { //N -// AppDelegate.toggleNightMode() -// } else if event.keyCode == 0 && ViewController.systemStatus != .busy && ViewController.systemStatus != .standby{ //A let i = ViewController.selectedFrequencyIndex if i > 0 { @@ -507,6 +552,9 @@ class ViewController: NSViewController{ } self.tprint("[QUICK BOOT]", raw: true) self.retrieveFrequencies() + if ViewController.defaults.integer(forKey: "narrator") == 1 { + SFX.speakWelcome() + } } } if appVersion != ViewController.defaults.string(forKey: "version"){ @@ -533,7 +581,7 @@ class ViewController: NSViewController{ } terminal.stringValue = "" ViewController.inMenu = false - + SFX.shutUp(speaker: true) ViewController.ticker.invalidate() ViewController.mainDisplaySwitchTimer.invalidate() } @@ -677,12 +725,14 @@ class ViewController: NSViewController{ case .playing: playbackControllerLight_pause.isHidden = true playbackControllerLight_error.isHidden = true + ViewController.controlCenterInfo.playbackState = .playing case .paused: playbackControllerLight_play.isHidden = true playbackControllerLight_pause.isHidden = false playbackControllerLight_loading.isHidden = true playbackControllerLight_error.isHidden = true playbackControllerLight_deepwave.isHidden = true + ViewController.controlCenterInfo.playbackState = .paused case .loading: playbackControllerLight_play.isHidden = true playbackControllerLight_pause.isHidden = true @@ -696,12 +746,16 @@ class ViewController: NSViewController{ playbackControllerLight_error.isHidden = false playbackControllerLight_deepwave.isHidden = true AutomneCore.notify(title: "🆘 Playback error encountered") + ViewController.controlCenterInfo.playbackState = .stopped + ViewController.controlCenterInfo.nowPlayingInfo = .none case .none: playbackControllerLight_play.isHidden = true playbackControllerLight_pause.isHidden = true playbackControllerLight_loading.isHidden = true playbackControllerLight_error.isHidden = true playbackControllerLight_deepwave.isHidden = true + ViewController.controlCenterInfo.playbackState = .stopped + ViewController.controlCenterInfo.nowPlayingInfo = .none } ViewController.playbackControllerState = to playbackControllerLight_stream.isHidden = !isStream @@ -749,6 +803,13 @@ class ViewController: NSViewController{ tprint((ViewController.setFrequency?.streamDescription)!, raw: true) tprint(" ***", raw: true) removeImage() + ViewController.controlCenterInfo.nowPlayingInfo = [ + "mediaType": MPMediaType.anyAudio, + "albumTitle": "Live", + "artist": "FM " + ((ViewController.setFrequency?.num) ?? "unknown"), + "title": (ViewController.setFrequency?.name) ?? "Unknown", + MPNowPlayingInfoPropertyIsLiveStream: 1.0 + ] } else { track = ViewController.playableQueue[from] ViewController.currentTrack = track @@ -774,6 +835,15 @@ class ViewController: NSViewController{ DispatchQueue.main.asyncAfter(deadline: .now() + 3, execute: { self.checkAndInvokeImage() }) + ViewController.controlCenterInfo.nowPlayingInfo = [ + "mediaType": MPMediaType.music, + "albumTitle": (ViewController.setFrequency?.name) ?? "Deepwave", + "artist": (track?.user?.username) ?? "Unknown", + "title": (track?.title) ?? "Unknown", + "playbackDuration": TimeInterval(exactly: track!.duration! / 1000)!, + "bookmarkTime": TimeInterval(exactly: 0.0)!, + MPNowPlayingInfoPropertyIsLiveStream: 0.0 + ] } let playerItem = AVPlayerItem.init(url: url) NotificationCenter.default.addObserver(self, selector: #selector(self.playerDidFinishPlaying(sender:)), @@ -790,7 +860,7 @@ class ViewController: NSViewController{ } else { ViewController.player.play() } - ViewController.mainDisplayState = .song + ViewController.mainDisplayState = .frequency setPlaybackControllerState(to: .playing) if ViewController.player.error != nil{ setSystemStatus(to: .error) @@ -816,6 +886,7 @@ class ViewController: NSViewController{ else{ setSystemStatus(to: .active) setPlaybackControllerState(to: .playing) + ViewController.controlCenterInfo.playbackState = .playing } } @@ -882,10 +953,9 @@ class ViewController: NSViewController{ self.switchFrequency(to: ViewController.retrievedFrequencies[0], index: 0) if log { self.tprint("SUCCESS") } if log {self.tprint("Retrieved " + String(ViewController.retrievedFrequencies.count) + " frequencies") } - if obj.message != nil { self.tprint(obj.message!, raw: true) } - AutomneAxioms.messages.append(obj.message ?? "Hey there") - self.TBLabel.stringValue = obj.message ?? "Ready" - AutomneAxioms.specialNarratives.append(contentsOf: obj.narratives ?? []) + self.tprint(obj.narratives![0]) + self.TBLabel.stringValue = obj.narratives![0] + AutomneAxioms.messages.append(contentsOf: obj.narratives ?? []) self.tprint("******", raw: true) self.tprint("Ready") if ViewController.defaults.integer(forKey: "appearance") == 0{ @@ -1103,6 +1173,9 @@ class ViewController: NSViewController{ func removeImage(){ terminalImage.image = nil ViewController.terminalImagePlaylistServed = false + if ViewController.controlCenterInfo.nowPlayingInfo != nil { + ViewController.controlCenterInfo.nowPlayingInfo!["artwork"] = .none + } } func hideImage(){ terminalImage.isHidden = true @@ -1114,6 +1187,8 @@ class ViewController: NSViewController{ func checkAndInvokeImage(){ if ViewController.currentTrack != nil && !ViewController.inMenu{ switch ViewController.defaults.integer(forKey: "artwork") { + case 0: + self.removeImage() case 1: if ViewController.currentTrack!.artwork_url != nil{ self.loadImage(from: URL(string: ((ViewController.currentTrack!.artwork_url)?.replacingOccurrences(of: "large", with: "t500x500"))!)!) @@ -1134,7 +1209,12 @@ class ViewController: NSViewController{ return } DispatchQueue.main.async { - self.terminalImage.image = NSImage(data: data) + let image = NSImage(data: data) + self.terminalImage.image = image + let CCArtwork = MPMediaItemArtwork.init(boundsSize: image!.size, requestHandler: { (size) -> NSImage in + return image! + }) + ViewController.controlCenterInfo.nowPlayingInfo!["artwork"] = CCArtwork } } }