From 7b8051a61eca46d1bba45640a3238f5ca24c0380 Mon Sep 17 00:00:00 2001 From: Vincent Neo <23420208+vincentneo@users.noreply.github.com> Date: Mon, 2 May 2022 00:17:37 +0800 Subject: [PATCH 01/32] a bunch more stuff/improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - now uses private API 😨 - instead of timer based periodic checking, it should now only check console every time theres a play/pause event (still buggy. some times it doesn't trigger) - should fix #6, #3, (part of) #1 --- Quality.xcodeproj/project.pbxproj | 53 ++++++++++++- .../xcshareddata/swiftpm/Package.resolved | 50 ++++++++++++ .../xcschemes/LosslessSwitcher.xcscheme | 78 +++++++++++++++++++ .../xcdebugger/Breakpoints_v2.xcbkptlist | 40 ++++++++++ Quality/AppDelegate.swift | 7 +- Quality/CMPlayerStuff.swift | 45 ++++++++++- Quality/Console.swift | 14 +++- Quality/MediaRemoteController.swift | 61 +++++++++++++++ Quality/MediaTrack.swift | 29 +++++++ Quality/OutputDevices.swift | 18 +++-- 10 files changed, 382 insertions(+), 13 deletions(-) create mode 100644 Quality.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 Quality.xcodeproj/xcshareddata/xcschemes/LosslessSwitcher.xcscheme create mode 100644 Quality.xcodeproj/xcuserdata/vincentneo.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist create mode 100644 Quality/MediaRemoteController.swift create mode 100644 Quality/MediaTrack.swift diff --git a/Quality.xcodeproj/project.pbxproj b/Quality.xcodeproj/project.pbxproj index 456f166..ef4100e 100644 --- a/Quality.xcodeproj/project.pbxproj +++ b/Quality.xcodeproj/project.pbxproj @@ -9,6 +9,10 @@ /* Begin PBXBuildFile section */ 1221F3F9280F10A3003E8B77 /* SimplyCoreAudio in Frameworks */ = {isa = PBXBuildFile; productRef = 1221F3F8280F10A3003E8B77 /* SimplyCoreAudio */; }; 1221F3FB280F1EEF003E8B77 /* OutputDevices.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1221F3FA280F1EEF003E8B77 /* OutputDevices.swift */; }; + 1234F508281E8372007EC9F5 /* PrivateMediaRemote in Frameworks */ = {isa = PBXBuildFile; productRef = 1234F507281E8372007EC9F5 /* PrivateMediaRemote */; }; + 1234F50A281E83D1007EC9F5 /* MediaRemote.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1234F509281E83D1007EC9F5 /* MediaRemote.framework */; }; + 1234F50E281E8F07007EC9F5 /* MediaTrack.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1234F50D281E8F07007EC9F5 /* MediaTrack.swift */; }; + 1234F510281E9520007EC9F5 /* MediaRemoteController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1234F50F281E9520007EC9F5 /* MediaRemoteController.swift */; }; 1254A79C2813FB9400241107 /* Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1254A79B2813FB9400241107 /* Defaults.swift */; }; 1272AA98280DBB4900FD72BA /* QualityApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1272AA97280DBB4900FD72BA /* QualityApp.swift */; }; 1272AA9A280DBB4900FD72BA /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1272AA99280DBB4900FD72BA /* ContentView.swift */; }; @@ -23,6 +27,9 @@ /* Begin PBXFileReference section */ 1221F3FA280F1EEF003E8B77 /* OutputDevices.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutputDevices.swift; sourceTree = ""; }; + 1234F509281E83D1007EC9F5 /* MediaRemote.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MediaRemote.framework; path = ../../../../../System/Library/PrivateFrameworks/MediaRemote.framework; sourceTree = ""; }; + 1234F50D281E8F07007EC9F5 /* MediaTrack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaTrack.swift; sourceTree = ""; }; + 1234F50F281E9520007EC9F5 /* MediaRemoteController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaRemoteController.swift; sourceTree = ""; }; 1254A79B2813FB9400241107 /* Defaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Defaults.swift; sourceTree = ""; }; 1254A79D2814024300241107 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 1272AA94280DBB4900FD72BA /* LosslessSwitcher.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = LosslessSwitcher.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -44,17 +51,28 @@ files = ( 1221F3F9280F10A3003E8B77 /* SimplyCoreAudio in Frameworks */, 1272AAB1280DC71B00FD72BA /* Sweep in Frameworks */, + 1234F50A281E83D1007EC9F5 /* MediaRemote.framework in Frameworks */, + 1234F508281E8372007EC9F5 /* PrivateMediaRemote in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 1234F4FD281E80A2007EC9F5 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 1234F509281E83D1007EC9F5 /* MediaRemote.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; 1272AA8B280DBB4900FD72BA = { isa = PBXGroup; children = ( 1272AA96280DBB4900FD72BA /* Quality */, 1272AA95280DBB4900FD72BA /* Products */, + 1234F4FD281E80A2007EC9F5 /* Frameworks */, ); sourceTree = ""; }; @@ -81,6 +99,8 @@ 1221F3FA280F1EEF003E8B77 /* OutputDevices.swift */, 1293436A28131591002E19A8 /* CurrentUser.swift */, 1254A79B2813FB9400241107 /* Defaults.swift */, + 1234F50D281E8F07007EC9F5 /* MediaTrack.swift */, + 1234F50F281E9520007EC9F5 /* MediaRemoteController.swift */, ); path = Quality; sourceTree = ""; @@ -112,6 +132,7 @@ packageProductDependencies = ( 1272AAB0280DC71B00FD72BA /* Sweep */, 1221F3F8280F10A3003E8B77 /* SimplyCoreAudio */, + 1234F507281E8372007EC9F5 /* PrivateMediaRemote */, ); productName = Quality; productReference = 1272AA94280DBB4900FD72BA /* LosslessSwitcher.app */; @@ -144,6 +165,7 @@ packageReferences = ( 1272AAAF280DC71B00FD72BA /* XCRemoteSwiftPackageReference "Sweep" */, 1221F3F7280F10A3003E8B77 /* XCRemoteSwiftPackageReference "SimplyCoreAudio" */, + 1234F504281E8372007EC9F5 /* XCRemoteSwiftPackageReference "MediaRemote" */, ); productRefGroup = 1272AA95280DBB4900FD72BA /* Products */; projectDirPath = ""; @@ -173,9 +195,11 @@ files = ( 12AFF5C12811AD40001CC6ED /* AppDelegate.swift in Sources */, 1254A79C2813FB9400241107 /* Defaults.swift in Sources */, + 1234F50E281E8F07007EC9F5 /* MediaTrack.swift in Sources */, 1221F3FB280F1EEF003E8B77 /* OutputDevices.swift in Sources */, 1272AAAE280DC68A00FD72BA /* CMPlayerStuff.swift in Sources */, 1272AAAC280DC5E900FD72BA /* Console.swift in Sources */, + 1234F510281E9520007EC9F5 /* MediaRemoteController.swift in Sources */, 1272AA9A280DBB4900FD72BA /* ContentView.swift in Sources */, 1293436B28131591002E19A8 /* CurrentUser.swift in Sources */, 1272AA98280DBB4900FD72BA /* QualityApp.swift in Sources */, @@ -306,10 +330,10 @@ CODE_SIGN_ENTITLEMENTS = Quality/Quality.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_ASSET_PATHS = "\"Quality/Preview Content\""; DEVELOPMENT_TEAM = 3X69W4AQD6; - ENABLE_HARDENED_RUNTIME = NO; + ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Quality/Info.plist; @@ -324,6 +348,10 @@ PRODUCT_NAME = LosslessSwitcher; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; + SYSTEM_FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(SYSTEM_LIBRARY_DIR)/PrivateFrameworks", + ); }; name = Debug; }; @@ -335,10 +363,10 @@ CODE_SIGN_ENTITLEMENTS = Quality/Quality.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_ASSET_PATHS = "\"Quality/Preview Content\""; DEVELOPMENT_TEAM = 3X69W4AQD6; - ENABLE_HARDENED_RUNTIME = NO; + ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Quality/Info.plist; @@ -353,6 +381,10 @@ PRODUCT_NAME = LosslessSwitcher; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; + SYSTEM_FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(SYSTEM_LIBRARY_DIR)/PrivateFrameworks", + ); }; name = Release; }; @@ -388,6 +420,14 @@ kind = branch; }; }; + 1234F504281E8372007EC9F5 /* XCRemoteSwiftPackageReference "MediaRemote" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/PrivateFrameworks/MediaRemote"; + requirement = { + kind = upToNextMinorVersion; + minimumVersion = 0.1.0; + }; + }; 1272AAAF280DC71B00FD72BA /* XCRemoteSwiftPackageReference "Sweep" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/JohnSundell/Sweep.git"; @@ -404,6 +444,11 @@ package = 1221F3F7280F10A3003E8B77 /* XCRemoteSwiftPackageReference "SimplyCoreAudio" */; productName = SimplyCoreAudio; }; + 1234F507281E8372007EC9F5 /* PrivateMediaRemote */ = { + isa = XCSwiftPackageProductDependency; + package = 1234F504281E8372007EC9F5 /* XCRemoteSwiftPackageReference "MediaRemote" */; + productName = PrivateMediaRemote; + }; 1272AAB0280DC71B00FD72BA /* Sweep */ = { isa = XCSwiftPackageProductDependency; package = 1272AAAF280DC71B00FD72BA /* XCRemoteSwiftPackageReference "Sweep" */; diff --git a/Quality.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Quality.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..27fe146 --- /dev/null +++ b/Quality.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,50 @@ +{ + "pins" : [ + { + "identity" : "mediaremote", + "kind" : "remoteSourceControl", + "location" : "https://github.com/PrivateFrameworks/MediaRemote", + "state" : { + "revision" : "ee5ae0cc17c0f759049d50e6ea95e1cf718dfda5", + "version" : "0.1.0" + } + }, + { + "identity" : "protocolbuffer", + "kind" : "remoteSourceControl", + "location" : "https://github.com/PrivateFrameworks/ProtocolBuffer", + "state" : { + "revision" : "20b692d09393389120bea65098ddf66129352bb7", + "version" : "0.1.0" + } + }, + { + "identity" : "simplycoreaudio", + "kind" : "remoteSourceControl", + "location" : "https://github.com/rnine/SimplyCoreAudio.git", + "state" : { + "branch" : "develop", + "revision" : "343d463cffef1f30458d02ce2dc441138e9e0134" + } + }, + { + "identity" : "sweep", + "kind" : "remoteSourceControl", + "location" : "https://github.com/JohnSundell/Sweep.git", + "state" : { + "revision" : "801c2878e4c6c5baf32fe132e1f3f3af6f9fd1b0", + "version" : "0.4.0" + } + }, + { + "identity" : "swift-atomics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-atomics.git", + "state" : { + "revision" : "3e95ba32cd1b4c877f6163e8eea54afc4e63bf9f", + "version" : "0.0.3" + } + } + ], + "version" : 2 +} diff --git a/Quality.xcodeproj/xcshareddata/xcschemes/LosslessSwitcher.xcscheme b/Quality.xcodeproj/xcshareddata/xcschemes/LosslessSwitcher.xcscheme new file mode 100644 index 0000000..cee0e06 --- /dev/null +++ b/Quality.xcodeproj/xcshareddata/xcschemes/LosslessSwitcher.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Quality.xcodeproj/xcuserdata/vincentneo.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/Quality.xcodeproj/xcuserdata/vincentneo.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist new file mode 100644 index 0000000..db9d38c --- /dev/null +++ b/Quality.xcodeproj/xcuserdata/vincentneo.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -0,0 +1,40 @@ + + + + + + + + + + + + + diff --git a/Quality/AppDelegate.swift b/Quality/AppDelegate.swift index cd69f5d..edcdf63 100644 --- a/Quality/AppDelegate.swift +++ b/Quality/AppDelegate.swift @@ -7,6 +7,7 @@ import Cocoa import SwiftUI +import PrivateMediaRemote class AppDelegate: NSObject, NSApplicationDelegate { @@ -14,6 +15,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { static private(set) var instance: AppDelegate! = nil private var outputDevices: OutputDevices! private let defaults = Defaults.shared + private var mrController: MediaRemoteController! var statusItem: NSStatusItem? @@ -51,6 +53,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { func applicationDidFinishLaunching(_ notification: Notification) { AppDelegate.instance = self outputDevices = OutputDevices() + mrController = MediaRemoteController(outputDevices: outputDevices) checkPermissions() @@ -75,8 +78,10 @@ class AppDelegate: NSObject, NSApplicationDelegate { self.statusItem?.menu = menu self.statusItem?.button?.title = "Loading..." self.statusItemDisplay() + + } - + func statusItemDisplay() { if defaults.userPreferIconStatusBarItem { self.statusItem?.button?.image = NSImage(systemSymbolName: "music.note", accessibilityDescription: "") diff --git a/Quality/CMPlayerStuff.swift b/Quality/CMPlayerStuff.swift index cbfd935..5507af4 100644 --- a/Quality/CMPlayerStuff.swift +++ b/Quality/CMPlayerStuff.swift @@ -13,6 +13,7 @@ struct CMPlayerStats { let sampleRate: Double // Hz let bitDepth: Int let date: Date + let priority: Int } class CMPlayerParser { @@ -48,7 +49,49 @@ class CMPlayerParser { if let sr = sampleRate, let bd = bitDepth { - let stat = CMPlayerStats(sampleRate: sr * 1000, bitDepth: bd, date: date) + let stat = CMPlayerStats(sampleRate: sr * 1000, bitDepth: bd, date: date, priority: 1) + stats.append(stat) + sampleRate = nil + bitDepth = nil + print("detected stat \(stat)") + break + } + + lastDate = date + + } + return stats + } + + static func parseCoreAudioConsoleLogs(_ entries: [SimpleConsole]) -> [CMPlayerStats] { + let kTimeDifferenceAcceptance = 5.0 // seconds + var lastDate: Date? + var sampleRate: Double? + var bitDepth: Int? + + var stats = [CMPlayerStats]() + + for entry in entries { + let date = entry.date + let rawMessage = entry.message + + if let lastDate = lastDate, date.timeIntervalSince(lastDate) > kTimeDifferenceAcceptance { + sampleRate = nil + bitDepth = nil + } + + if rawMessage.contains("Input format: ") { + if let subSampleRate = rawMessage.firstSubstring(between: "ch, ", and: " Hz") { + let strSampleRate = String(subSampleRate) + sampleRate = Double(strSampleRate) + } + + bitDepth = 24 // not important anymore, just putting as placeholder, at least until there's a way to set bit depth with Core Audio. + } + + if let sr = sampleRate, + let bd = bitDepth { + let stat = CMPlayerStats(sampleRate: sr, bitDepth: bd, date: date, priority: 10) stats.append(stat) sampleRate = nil bitDepth = nil diff --git a/Quality/Console.swift b/Quality/Console.swift index 6bd0c7a..c2254dc 100644 --- a/Quality/Console.swift +++ b/Quality/Console.swift @@ -14,15 +14,25 @@ struct SimpleConsole { let message: String } +enum EntryType: String { + case music = "com.apple.Music" + case coreAudio = "com.apple.coreaudio" + + var predicate: NSPredicate { + NSPredicate(format: "subsystem = %@", rawValue) + } +} + class Console { - static func getRecentEntries() throws -> [SimpleConsole] { + static func getRecentEntries(type: EntryType) throws -> [SimpleConsole] { var messages = [SimpleConsole]() let store = try OSLogStore.local() let duration = store.position(timeIntervalSinceEnd: -5.0) - let entries = try store.getEntries(with: [], at: duration, matching: NSPredicate(format: "subsystem = %@", "com.apple.Music")) + let entries = try store.getEntries(with: [], at: duration, matching: type.predicate) // for some reason AnySequence to Array turns it into a empty array? for entry in entries { let consoleMessage = SimpleConsole(date: entry.date, message: entry.composedMessage) + //print((date: entry.date, message: entry.composedMessage)) messages.append(consoleMessage) } diff --git a/Quality/MediaRemoteController.swift b/Quality/MediaRemoteController.swift new file mode 100644 index 0000000..78e10d7 --- /dev/null +++ b/Quality/MediaRemoteController.swift @@ -0,0 +1,61 @@ +// +// MediaRemoteController.swift +// LosslessSwitcher +// +// Created by Vincent Neo on 1/5/22. +// + +import Cocoa +import Combine +import PrivateMediaRemote + +fileprivate let kMusicAppBundle = "com.apple.Music" + +class MediaRemoteController { + + private var infoChangedCancellable: AnyCancellable? + private var queueChangedCancellable: AnyCancellable? + + private var previousTrack: MediaTrack? + + init(outputDevices: OutputDevices) { + infoChangedCancellable = NotificationCenter.default.publisher(for: NSNotification.Name.mrMediaRemoteNowPlayingInfoDidChange) + .throttle(for: .seconds(3), scheduler: DispatchQueue.main, latest: true) + .sink(receiveValue: { notification in + print("Info Changed Notification Received") + MRMediaRemoteGetNowPlayingInfo(.main) { info in + if let info = info as? [String : Any] { + let currentTrack = MediaTrack(mediaRemote: info) + if currentTrack != self.previousTrack { + self.send(command: MRMediaRemoteCommandPause, ifBundleMatches: kMusicAppBundle) { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) { + outputDevices.switchLatestSampleRate() + self.send(command: MRMediaRemoteCommandPlay, ifBundleMatches: kMusicAppBundle) {} + } + } + } + self.previousTrack = currentTrack + } + } + }) +// queueChangedCancellable = NotificationCenter.default.publisher(for: NSNotification.Name.mrNowPlayingPlaybackQueueChanged) +// //.throttle(for: .seconds(3), scheduler: DispatchQueue.main, latest: false) +// .removeDuplicates() +// .sink(receiveValue: { notification in +// print("Queue Changed Notification Received") +// }) + + MRMediaRemoteRegisterForNowPlayingNotifications(.main) + //NotificationCenter.default.addObserver(self, selector: #selector(x(notification:)), name: NSNotification.Name.mrMediaRemoteNowPlayingInfoDidChange, object: nil) + } + + func send(command: MRMediaRemoteCommand, ifBundleMatches bundleId: String, completion: @escaping () -> ()) { + MRMediaRemoteGetNowPlayingClient(.main) { client in + guard let client = client else { return } + if client.bundleIdentifier == bundleId { + MRMediaRemoteSendCommand(command, nil) + } + completion() + } + } +} diff --git a/Quality/MediaTrack.swift b/Quality/MediaTrack.swift new file mode 100644 index 0000000..1878aec --- /dev/null +++ b/Quality/MediaTrack.swift @@ -0,0 +1,29 @@ +// +// MediaTrack.swift +// LosslessSwitcher +// +// Created by Vincent Neo on 1/5/22. +// + +import Foundation +import PrivateMediaRemote + +struct MediaTrack: Equatable, Hashable { + + let isMusicApp: Bool + let id: String? + + let title: String? + let album: String? + let artist: String? + let trackNumber: String? + + init(mediaRemote info: [String : Any]) { + self.id = info[kMRMediaRemoteNowPlayingInfoUniqueIdentifier] as? String + self.isMusicApp = info[kMRMediaRemoteNowPlayingInfoIsMusicApp] as? Bool ?? false + self.title = info[kMRMediaRemoteNowPlayingInfoTitle] as? String + self.album = info[kMRMediaRemoteNowPlayingInfoAlbum] as? String + self.artist = info[kMRMediaRemoteNowPlayingInfoArtist] as? String + self.trackNumber = info[kMRMediaRemoteNowPlayingInfoTrackNumber] as? String + } +} diff --git a/Quality/OutputDevices.swift b/Quality/OutputDevices.swift index 3c8bea8..f911105 100644 --- a/Quality/OutputDevices.swift +++ b/Quality/OutputDevices.swift @@ -23,6 +23,8 @@ class OutputDevices: ObservableObject { private var timerCancellable: AnyCancellable? private var consoleQueue = DispatchQueue(label: "consoleQueue", qos: .userInteractive) + private var previousSampleRate: Float64? + init() { self.outputDevices = self.coreAudio.allOutputDevices self.defaultOutputDevice = self.coreAudio.defaultOutputDevice @@ -41,7 +43,7 @@ class OutputDevices: ObservableObject { timerCancellable = timer.sink(receiveValue: { _ in self.consoleQueue.async { - self.switchLatestSampleRate() + //self.switchLatestSampleRate() } }) } @@ -61,11 +63,16 @@ class OutputDevices: ObservableObject { func switchLatestSampleRate() { do { - let musicLog = try Console.getRecentEntries() - let cmStats = CMPlayerParser.parseMusicConsoleLogs(musicLog) + var allStats = [CMPlayerStats]() + let musicLogs = try Console.getRecentEntries(type: .music) + let coreAudioLogs = try Console.getRecentEntries(type: .coreAudio) + allStats.append(contentsOf: CMPlayerParser.parseMusicConsoleLogs(musicLogs)) + allStats.append(contentsOf: CMPlayerParser.parseCoreAudioConsoleLogs(coreAudioLogs)) + allStats.sort(by: {$0.priority > $1.priority}) + print(allStats) let defaultDevice = self.defaultOutputDevice - if let first = cmStats.first, let supported = defaultDevice?.nominalSampleRates { + if let first = allStats.first, let supported = defaultDevice?.nominalSampleRates { let sampleRate = Float64(first.sampleRate) // https://stackoverflow.com/a/65060134 let nearest = supported.enumerated().min(by: { @@ -73,7 +80,7 @@ class OutputDevices: ObservableObject { }) if let nearest = nearest { let nearestSampleRate = nearest.element - if nearestSampleRate != defaultDevice?.nominalSampleRate { + if nearestSampleRate != previousSampleRate { defaultDevice?.setNominalSampleRate(nearestSampleRate) self.updateSampleRate(nearestSampleRate) } @@ -86,6 +93,7 @@ class OutputDevices: ObservableObject { } func updateSampleRate(_ sampleRate: Float64) { + self.previousSampleRate = sampleRate DispatchQueue.main.async { let readableSampleRate = sampleRate / 1000 self.currentSampleRate = readableSampleRate From 86edbda0710cf2e4078686cb81159e8a1f97d714 Mon Sep 17 00:00:00 2001 From: Vincent Neo <23420208+vincentneo@users.noreply.github.com> Date: Mon, 2 May 2022 18:53:21 +0800 Subject: [PATCH 02/32] remove play/pause, fix #8, show version/build --- Quality.xcodeproj/project.pbxproj | 12 +++++++---- .../xcdebugger/Breakpoints_v2.xcbkptlist | 16 -------------- Quality/AppDelegate.swift | 10 +++++++++ Quality/AppVersion.swift | 11 ++++++++++ Quality/ContentView.swift | 2 +- Quality/MediaRemoteController.swift | 21 ++++++++----------- 6 files changed, 39 insertions(+), 33 deletions(-) create mode 100644 Quality/AppVersion.swift diff --git a/Quality.xcodeproj/project.pbxproj b/Quality.xcodeproj/project.pbxproj index ef4100e..ee7918f 100644 --- a/Quality.xcodeproj/project.pbxproj +++ b/Quality.xcodeproj/project.pbxproj @@ -21,6 +21,7 @@ 1272AAAC280DC5E900FD72BA /* Console.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1272AAAB280DC5E900FD72BA /* Console.swift */; }; 1272AAAE280DC68A00FD72BA /* CMPlayerStuff.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1272AAAD280DC68A00FD72BA /* CMPlayerStuff.swift */; }; 1272AAB1280DC71B00FD72BA /* Sweep in Frameworks */ = {isa = PBXBuildFile; productRef = 1272AAB0280DC71B00FD72BA /* Sweep */; }; + 127C972D281FCF000087313B /* AppVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 127C972C281FCF000087313B /* AppVersion.swift */; }; 1293436B28131591002E19A8 /* CurrentUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1293436A28131591002E19A8 /* CurrentUser.swift */; }; 12AFF5C12811AD40001CC6ED /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12AFF5C02811AD40001CC6ED /* AppDelegate.swift */; }; /* End PBXBuildFile section */ @@ -40,6 +41,7 @@ 1272AAA0280DBB4B00FD72BA /* Quality.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Quality.entitlements; sourceTree = ""; }; 1272AAAB280DC5E900FD72BA /* Console.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Console.swift; sourceTree = ""; }; 1272AAAD280DC68A00FD72BA /* CMPlayerStuff.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CMPlayerStuff.swift; sourceTree = ""; }; + 127C972C281FCF000087313B /* AppVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppVersion.swift; sourceTree = ""; }; 1293436A28131591002E19A8 /* CurrentUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentUser.swift; sourceTree = ""; }; 12AFF5C02811AD40001CC6ED /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -101,6 +103,7 @@ 1254A79B2813FB9400241107 /* Defaults.swift */, 1234F50D281E8F07007EC9F5 /* MediaTrack.swift */, 1234F50F281E9520007EC9F5 /* MediaRemoteController.swift */, + 127C972C281FCF000087313B /* AppVersion.swift */, ); path = Quality; sourceTree = ""; @@ -203,6 +206,7 @@ 1272AA9A280DBB4900FD72BA /* ContentView.swift in Sources */, 1293436B28131591002E19A8 /* CurrentUser.swift in Sources */, 1272AA98280DBB4900FD72BA /* QualityApp.swift in Sources */, + 127C972D281FCF000087313B /* AppVersion.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -330,7 +334,7 @@ CODE_SIGN_ENTITLEMENTS = Quality/Quality.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 4; DEVELOPMENT_ASSET_PATHS = "\"Quality/Preview Content\""; DEVELOPMENT_TEAM = 3X69W4AQD6; ENABLE_HARDENED_RUNTIME = YES; @@ -343,7 +347,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.4; - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 1.1; PRODUCT_BUNDLE_IDENTIFIER = "com.vincent-neo.LosslessSwitcher"; PRODUCT_NAME = LosslessSwitcher; SWIFT_EMIT_LOC_STRINGS = YES; @@ -363,7 +367,7 @@ CODE_SIGN_ENTITLEMENTS = Quality/Quality.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 4; DEVELOPMENT_ASSET_PATHS = "\"Quality/Preview Content\""; DEVELOPMENT_TEAM = 3X69W4AQD6; ENABLE_HARDENED_RUNTIME = YES; @@ -376,7 +380,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.4; - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 1.1; PRODUCT_BUNDLE_IDENTIFIER = "com.vincent-neo.LosslessSwitcher"; PRODUCT_NAME = LosslessSwitcher; SWIFT_EMIT_LOC_STRINGS = YES; diff --git a/Quality.xcodeproj/xcuserdata/vincentneo.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/Quality.xcodeproj/xcuserdata/vincentneo.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist index db9d38c..1eac07c 100644 --- a/Quality.xcodeproj/xcuserdata/vincentneo.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist +++ b/Quality.xcodeproj/xcuserdata/vincentneo.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -20,21 +20,5 @@ landmarkType = "24"> - - - - diff --git a/Quality/AppDelegate.swift b/Quality/AppDelegate.swift index edcdf63..1eb4c2e 100644 --- a/Quality/AppDelegate.swift +++ b/Quality/AppDelegate.swift @@ -71,6 +71,16 @@ class AppDelegate: NSObject, NSApplicationDelegate { let showSampleRateItem = NSMenuItem(title: defaults.statusBarItemTitle, action: #selector(toggleSampleRate(item:)), keyEquivalent: "") menu.addItem(showSampleRateItem) + + let aboutItem = NSMenuItem(title: "About", action: nil, keyEquivalent: "") + let versionItem = NSMenuItem(title: "Version - \(currentVersion)", action: nil, keyEquivalent: "") + let buildItem = NSMenuItem(title: "Build - \(currentBuild)", action: nil, keyEquivalent: "") + + aboutItem.submenu = NSMenu() + aboutItem.submenu?.addItem(versionItem) + aboutItem.submenu?.addItem(buildItem) + menu.addItem(aboutItem) + let quitItem = NSMenuItem(title: "Quit", action: #selector(NSApp.terminate(_:)), keyEquivalent: "") menu.addItem(quitItem) diff --git a/Quality/AppVersion.swift b/Quality/AppVersion.swift new file mode 100644 index 0000000..67b66b7 --- /dev/null +++ b/Quality/AppVersion.swift @@ -0,0 +1,11 @@ +// +// AppVersion.swift +// LosslessSwitcher +// +// Created by Vincent Neo on 2/5/22. +// + +import Foundation + +let currentBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as! String +let currentVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as! String diff --git a/Quality/ContentView.swift b/Quality/ContentView.swift index 1357e17..27c12c8 100644 --- a/Quality/ContentView.swift +++ b/Quality/ContentView.swift @@ -21,7 +21,7 @@ struct ContentView: View { } if let device = outputDevices.defaultOutputDevice { Text(device.name) - .font(.system(size: 16, weight: .regular, design: .default)) + .font(.system(size: 14.5, weight: .regular, design: .default)) .foregroundColor(.secondary) } } diff --git a/Quality/MediaRemoteController.swift b/Quality/MediaRemoteController.swift index 78e10d7..d43231b 100644 --- a/Quality/MediaRemoteController.swift +++ b/Quality/MediaRemoteController.swift @@ -27,26 +27,23 @@ class MediaRemoteController { if let info = info as? [String : Any] { let currentTrack = MediaTrack(mediaRemote: info) if currentTrack != self.previousTrack { - self.send(command: MRMediaRemoteCommandPause, ifBundleMatches: kMusicAppBundle) { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) { - outputDevices.switchLatestSampleRate() - self.send(command: MRMediaRemoteCommandPlay, ifBundleMatches: kMusicAppBundle) {} - } + //self.send(command: MRMediaRemoteCommandPause, ifBundleMatches: kMusicAppBundle) { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) { + outputDevices.switchLatestSampleRate() + //self.send(command: MRMediaRemoteCommandPlay, ifBundleMatches: kMusicAppBundle) {} } + DispatchQueue.main.asyncAfter(deadline: .now() + 1.6) { + outputDevices.switchLatestSampleRate() + //self.send(command: MRMediaRemoteCommandPlay, ifBundleMatches: kMusicAppBundle) {} + } + //} } self.previousTrack = currentTrack } } }) -// queueChangedCancellable = NotificationCenter.default.publisher(for: NSNotification.Name.mrNowPlayingPlaybackQueueChanged) -// //.throttle(for: .seconds(3), scheduler: DispatchQueue.main, latest: false) -// .removeDuplicates() -// .sink(receiveValue: { notification in -// print("Queue Changed Notification Received") -// }) MRMediaRemoteRegisterForNowPlayingNotifications(.main) - //NotificationCenter.default.addObserver(self, selector: #selector(x(notification:)), name: NSNotification.Name.mrMediaRemoteNowPlayingInfoDidChange, object: nil) } func send(command: MRMediaRemoteCommand, ifBundleMatches bundleId: String, completion: @escaping () -> ()) { From a8cdd07bd09c9b4291e1fe7fcbb42d171036247b Mon Sep 17 00:00:00 2001 From: Vincent Neo <23420208+vincentneo@users.noreply.github.com> Date: Tue, 3 May 2022 00:06:01 +0800 Subject: [PATCH 03/32] another alternative detection message --- Quality.xcodeproj/project.pbxproj | 4 ++-- Quality/CMPlayerStuff.swift | 37 +++++++++++++++++++++++++++++ Quality/Console.swift | 3 ++- Quality/MediaRemoteController.swift | 13 +++++----- Quality/OutputDevices.swift | 16 +++++++++---- 5 files changed, 59 insertions(+), 14 deletions(-) diff --git a/Quality.xcodeproj/project.pbxproj b/Quality.xcodeproj/project.pbxproj index ee7918f..3c794c2 100644 --- a/Quality.xcodeproj/project.pbxproj +++ b/Quality.xcodeproj/project.pbxproj @@ -334,7 +334,7 @@ CODE_SIGN_ENTITLEMENTS = Quality/Quality.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 5; DEVELOPMENT_ASSET_PATHS = "\"Quality/Preview Content\""; DEVELOPMENT_TEAM = 3X69W4AQD6; ENABLE_HARDENED_RUNTIME = YES; @@ -367,7 +367,7 @@ CODE_SIGN_ENTITLEMENTS = Quality/Quality.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 5; DEVELOPMENT_ASSET_PATHS = "\"Quality/Preview Content\""; DEVELOPMENT_TEAM = 3X69W4AQD6; ENABLE_HARDENED_RUNTIME = YES; diff --git a/Quality/CMPlayerStuff.swift b/Quality/CMPlayerStuff.swift index 5507af4..9fcf589 100644 --- a/Quality/CMPlayerStuff.swift +++ b/Quality/CMPlayerStuff.swift @@ -104,4 +104,41 @@ class CMPlayerParser { } return stats } + + static func parseCoreMediaConsoleLogs(_ entries: [SimpleConsole]) -> [CMPlayerStats] { + let kTimeDifferenceAcceptance = 5.0 // seconds + var lastDate: Date? + var sampleRate: Double? + let bitDepth = 24 // not important anymore, just putting as placeholder, at least until there's a way to set bit depth with Core Audio. + + var stats = [CMPlayerStats]() + + for entry in entries { + let date = entry.date + let rawMessage = entry.message + + if let lastDate = lastDate, date.timeIntervalSince(lastDate) > kTimeDifferenceAcceptance { + sampleRate = nil + } + + if rawMessage.contains("Creating AudioQueue") { + if let subSampleRate = rawMessage.firstSubstring(between: "sampleRate:", and: .end) { + let strSampleRate = String(subSampleRate) + sampleRate = Double(strSampleRate) + } + } + + if let sr = sampleRate { + let stat = CMPlayerStats(sampleRate: sr, bitDepth: bitDepth, date: date, priority: 10) + stats.append(stat) + sampleRate = nil + print("detected stat \(stat)") + break + } + + lastDate = date + + } + return stats + } } diff --git a/Quality/Console.swift b/Quality/Console.swift index c2254dc..090ad89 100644 --- a/Quality/Console.swift +++ b/Quality/Console.swift @@ -17,6 +17,7 @@ struct SimpleConsole { enum EntryType: String { case music = "com.apple.Music" case coreAudio = "com.apple.coreaudio" + case coreMedia = "com.apple.coremedia" var predicate: NSPredicate { NSPredicate(format: "subsystem = %@", rawValue) @@ -27,7 +28,7 @@ class Console { static func getRecentEntries(type: EntryType) throws -> [SimpleConsole] { var messages = [SimpleConsole]() let store = try OSLogStore.local() - let duration = store.position(timeIntervalSinceEnd: -5.0) + let duration = store.position(timeIntervalSinceEnd: -2.0) let entries = try store.getEntries(with: [], at: duration, matching: type.predicate) // for some reason AnySequence to Array turns it into a empty array? for entry in entries { diff --git a/Quality/MediaRemoteController.swift b/Quality/MediaRemoteController.swift index d43231b..d3f8ac5 100644 --- a/Quality/MediaRemoteController.swift +++ b/Quality/MediaRemoteController.swift @@ -22,21 +22,22 @@ class MediaRemoteController { infoChangedCancellable = NotificationCenter.default.publisher(for: NSNotification.Name.mrMediaRemoteNowPlayingInfoDidChange) .throttle(for: .seconds(3), scheduler: DispatchQueue.main, latest: true) .sink(receiveValue: { notification in + //print(notification) print("Info Changed Notification Received") MRMediaRemoteGetNowPlayingInfo(.main) { info in if let info = info as? [String : Any] { let currentTrack = MediaTrack(mediaRemote: info) if currentTrack != self.previousTrack { //self.send(command: MRMediaRemoteCommandPause, ifBundleMatches: kMusicAppBundle) { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) { + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { outputDevices.switchLatestSampleRate() //self.send(command: MRMediaRemoteCommandPlay, ifBundleMatches: kMusicAppBundle) {} } - DispatchQueue.main.asyncAfter(deadline: .now() + 1.6) { - outputDevices.switchLatestSampleRate() - //self.send(command: MRMediaRemoteCommandPlay, ifBundleMatches: kMusicAppBundle) {} - } - //} +// DispatchQueue.main.asyncAfter(deadline: .now() + 2) { +// outputDevices.switchLatestSampleRate() +// //self.send(command: MRMediaRemoteCommandPlay, ifBundleMatches: kMusicAppBundle) {} +// } +// //} } self.previousTrack = currentTrack } diff --git a/Quality/OutputDevices.swift b/Quality/OutputDevices.swift index f911105..4db3d81 100644 --- a/Quality/OutputDevices.swift +++ b/Quality/OutputDevices.swift @@ -19,7 +19,7 @@ class OutputDevices: ObservableObject { private var changesCancellable: AnyCancellable? private var defaultChangesCancellable: AnyCancellable? - private let timer = Timer.publish(every: 2, on: .main, in: .common).autoconnect() + private let timer = Timer.publish(every: 2, on: .main, in: .common)//.autoconnect() private var timerCancellable: AnyCancellable? private var consoleQueue = DispatchQueue(label: "consoleQueue", qos: .userInteractive) @@ -52,7 +52,7 @@ class OutputDevices: ObservableObject { changesCancellable?.cancel() defaultChangesCancellable?.cancel() timerCancellable?.cancel() - timer.upstream.connect().cancel() + //timer.upstream.connect().cancel() } func getDeviceSampleRate() { @@ -61,13 +61,14 @@ class OutputDevices: ObservableObject { self.updateSampleRate(sampleRate) } - func switchLatestSampleRate() { + func switchLatestSampleRate(recursion: Bool = false) { do { var allStats = [CMPlayerStats]() let musicLogs = try Console.getRecentEntries(type: .music) - let coreAudioLogs = try Console.getRecentEntries(type: .coreAudio) + //let coreAudioLogs = try Console.getRecentEntries(type: .coreAudio) + let coreMediaLogs = try Console.getRecentEntries(type: .coreMedia) allStats.append(contentsOf: CMPlayerParser.parseMusicConsoleLogs(musicLogs)) - allStats.append(contentsOf: CMPlayerParser.parseCoreAudioConsoleLogs(coreAudioLogs)) + allStats.append(contentsOf: CMPlayerParser.parseCoreMediaConsoleLogs(coreMediaLogs)) allStats.sort(by: {$0.priority > $1.priority}) print(allStats) @@ -86,6 +87,11 @@ class OutputDevices: ObservableObject { } } } + else if !recursion { + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + self.switchLatestSampleRate(recursion: true) + } + } } catch { print(error) From d7705abef77465904f049a3954d48c7c02e2fa42 Mon Sep 17 00:00:00 2001 From: Vincent Neo <23420208+vincentneo@users.noreply.github.com> Date: Sun, 26 Jun 2022 17:05:38 +0800 Subject: [PATCH 04/32] multiple additional strategies --- Quality.xcodeproj/project.pbxproj | 4 +- Quality/AppDelegate.swift | 2 - Quality/CMPlayerStuff.swift | 2 +- Quality/Console.swift | 2 +- Quality/MediaRemoteController.swift | 23 +++++++---- Quality/OutputDevices.swift | 64 +++++++++++++++++++++++++---- 6 files changed, 75 insertions(+), 22 deletions(-) diff --git a/Quality.xcodeproj/project.pbxproj b/Quality.xcodeproj/project.pbxproj index 3c794c2..63be661 100644 --- a/Quality.xcodeproj/project.pbxproj +++ b/Quality.xcodeproj/project.pbxproj @@ -334,7 +334,7 @@ CODE_SIGN_ENTITLEMENTS = Quality/Quality.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; DEVELOPMENT_ASSET_PATHS = "\"Quality/Preview Content\""; DEVELOPMENT_TEAM = 3X69W4AQD6; ENABLE_HARDENED_RUNTIME = YES; @@ -367,7 +367,7 @@ CODE_SIGN_ENTITLEMENTS = Quality/Quality.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; DEVELOPMENT_ASSET_PATHS = "\"Quality/Preview Content\""; DEVELOPMENT_TEAM = 3X69W4AQD6; ENABLE_HARDENED_RUNTIME = YES; diff --git a/Quality/AppDelegate.swift b/Quality/AppDelegate.swift index 1eb4c2e..e9e52e8 100644 --- a/Quality/AppDelegate.swift +++ b/Quality/AppDelegate.swift @@ -88,8 +88,6 @@ class AppDelegate: NSObject, NSApplicationDelegate { self.statusItem?.menu = menu self.statusItem?.button?.title = "Loading..." self.statusItemDisplay() - - } func statusItemDisplay() { diff --git a/Quality/CMPlayerStuff.swift b/Quality/CMPlayerStuff.swift index 9fcf589..73a4033 100644 --- a/Quality/CMPlayerStuff.swift +++ b/Quality/CMPlayerStuff.swift @@ -91,7 +91,7 @@ class CMPlayerParser { if let sr = sampleRate, let bd = bitDepth { - let stat = CMPlayerStats(sampleRate: sr, bitDepth: bd, date: date, priority: 10) + let stat = CMPlayerStats(sampleRate: sr, bitDepth: bd, date: date, priority: 5) stats.append(stat) sampleRate = nil bitDepth = nil diff --git a/Quality/Console.swift b/Quality/Console.swift index 090ad89..d7092fa 100644 --- a/Quality/Console.swift +++ b/Quality/Console.swift @@ -28,7 +28,7 @@ class Console { static func getRecentEntries(type: EntryType) throws -> [SimpleConsole] { var messages = [SimpleConsole]() let store = try OSLogStore.local() - let duration = store.position(timeIntervalSinceEnd: -2.0) + let duration = store.position(timeIntervalSinceEnd: -3.0) let entries = try store.getEntries(with: [], at: duration, matching: type.predicate) // for some reason AnySequence to Array turns it into a empty array? for entry in entries { diff --git a/Quality/MediaRemoteController.swift b/Quality/MediaRemoteController.swift index d3f8ac5..2137fd9 100644 --- a/Quality/MediaRemoteController.swift +++ b/Quality/MediaRemoteController.swift @@ -16,30 +16,35 @@ class MediaRemoteController { private var infoChangedCancellable: AnyCancellable? private var queueChangedCancellable: AnyCancellable? - private var previousTrack: MediaTrack? + //private var previousTrack: MediaTrack? init(outputDevices: OutputDevices) { infoChangedCancellable = NotificationCenter.default.publisher(for: NSNotification.Name.mrMediaRemoteNowPlayingInfoDidChange) - .throttle(for: .seconds(3), scheduler: DispatchQueue.main, latest: true) + .throttle(for: .seconds(1), scheduler: DispatchQueue.main, latest: true) .sink(receiveValue: { notification in //print(notification) print("Info Changed Notification Received") MRMediaRemoteGetNowPlayingInfo(.main) { info in if let info = info as? [String : Any] { let currentTrack = MediaTrack(mediaRemote: info) - if currentTrack != self.previousTrack { - //self.send(command: MRMediaRemoteCommandPause, ifBundleMatches: kMusicAppBundle) { - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - outputDevices.switchLatestSampleRate() - //self.send(command: MRMediaRemoteCommandPlay, ifBundleMatches: kMusicAppBundle) {} + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + print("Current Track \(outputDevices.currentTrack?.title ?? "nil"), previous: \(outputDevices.previousTrack?.title ?? "nil"), isSame: \(outputDevices.previousTrack == outputDevices.currentTrack)") + outputDevices.previousTrack = outputDevices.currentTrack + outputDevices.currentTrack = currentTrack + if outputDevices.previousTrack != outputDevices.currentTrack { + outputDevices.renewTimer() } + outputDevices.switchLatestSampleRate() + } +// if currentTrack != self.previousTrack { +// self.send(command: MRMediaRemoteCommandPause, ifBundleMatches: kMusicAppBundle) { // DispatchQueue.main.asyncAfter(deadline: .now() + 2) { // outputDevices.switchLatestSampleRate() // //self.send(command: MRMediaRemoteCommandPlay, ifBundleMatches: kMusicAppBundle) {} // } // //} - } - self.previousTrack = currentTrack +// } + //self.previousTrack = currentTrack } } }) diff --git a/Quality/OutputDevices.swift b/Quality/OutputDevices.swift index 4db3d81..786b6ae 100644 --- a/Quality/OutputDevices.swift +++ b/Quality/OutputDevices.swift @@ -18,12 +18,16 @@ class OutputDevices: ObservableObject { private var changesCancellable: AnyCancellable? private var defaultChangesCancellable: AnyCancellable? - - private let timer = Timer.publish(every: 2, on: .main, in: .common)//.autoconnect() private var timerCancellable: AnyCancellable? private var consoleQueue = DispatchQueue(label: "consoleQueue", qos: .userInteractive) private var previousSampleRate: Float64? + var trackAndSample = [MediaTrack : Float64]() + var previousTrack: MediaTrack? + var currentTrack: MediaTrack? + + var timerActive = false + var timerCalls = 0 init() { self.outputDevices = self.coreAudio.allOutputDevices @@ -41,11 +45,6 @@ class OutputDevices: ObservableObject { self.getDeviceSampleRate() }) - timerCancellable = timer.sink(receiveValue: { _ in - self.consoleQueue.async { - //self.switchLatestSampleRate() - } - }) } deinit { @@ -55,6 +54,27 @@ class OutputDevices: ObservableObject { //timer.upstream.connect().cancel() } + func renewTimer() { + if timerCancellable != nil { return } + timerCancellable = Timer + .publish(every: 2, on: .main, in: .default) + .autoconnect() + .sink { _ in + print("cancellable \(self.timerCancellable), times \(self.timerCalls)") + if self.timerCalls == 5 { + self.timerCalls = 0 + self.timerCancellable?.cancel() + self.timerCancellable = nil + } + else { + self.timerCalls += 1 + self.consoleQueue.async { + self.switchLatestSampleRate() + } + } + } + } + func getDeviceSampleRate() { let defaultDevice = self.defaultOutputDevice guard let sampleRate = defaultDevice?.nominalSampleRate else { return } @@ -68,6 +88,7 @@ class OutputDevices: ObservableObject { //let coreAudioLogs = try Console.getRecentEntries(type: .coreAudio) let coreMediaLogs = try Console.getRecentEntries(type: .coreMedia) allStats.append(contentsOf: CMPlayerParser.parseMusicConsoleLogs(musicLogs)) + //allStats.append(contentsOf: CMPlayerParser.parseCoreAudioConsoleLogs(coreAudioLogs)) allStats.append(contentsOf: CMPlayerParser.parseCoreMediaConsoleLogs(coreMediaLogs)) allStats.sort(by: {$0.priority > $1.priority}) @@ -75,6 +96,18 @@ class OutputDevices: ObservableObject { let defaultDevice = self.defaultOutputDevice if let first = allStats.first, let supported = defaultDevice?.nominalSampleRates { let sampleRate = Float64(first.sampleRate) + + if self.currentTrack == self.previousTrack, let prevSampleRate = currentSampleRate, prevSampleRate > sampleRate { + print("same track, prev sample rate is higher") + return + } + + if sampleRate == 48000 { + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + self.switchLatestSampleRate(recursion: true) + } + } + // https://stackoverflow.com/a/65060134 let nearest = supported.enumerated().min(by: { abs($0.element - sampleRate) < abs($1.element - sampleRate) @@ -84,6 +117,9 @@ class OutputDevices: ObservableObject { if nearestSampleRate != previousSampleRate { defaultDevice?.setNominalSampleRate(nearestSampleRate) self.updateSampleRate(nearestSampleRate) + if let currentTrack = currentTrack { + self.trackAndSample[currentTrack] = nearestSampleRate + } } } } @@ -92,6 +128,20 @@ class OutputDevices: ObservableObject { self.switchLatestSampleRate(recursion: true) } } + else { +// print("cache \(self.trackAndSample)") + if self.currentTrack == self.previousTrack { + print("same track, ignore cache") + return + } + if let currentTrack = currentTrack, let cachedSampleRate = trackAndSample[currentTrack] { + print("using cached data") + if cachedSampleRate != previousSampleRate { + defaultDevice?.setNominalSampleRate(cachedSampleRate) + self.updateSampleRate(cachedSampleRate) + } + } + } } catch { print(error) From 04bb574e95d9269efa54a88bb1afbf245a186a27 Mon Sep 17 00:00:00 2001 From: Vincent Neo <23420208+vincentneo@users.noreply.github.com> Date: Sun, 26 Jun 2022 19:21:22 +0800 Subject: [PATCH 05/32] Selectable audio device override (#13) fix #13 --- Quality.xcodeproj/project.pbxproj | 8 ++++-- Quality/AppDelegate.swift | 43 ++++++++++++++++++++++++++++++- Quality/ContentView.swift | 2 +- Quality/DeviceMenuItem.swift | 22 ++++++++++++++++ Quality/OutputDevices.swift | 12 ++++++--- 5 files changed, 80 insertions(+), 7 deletions(-) create mode 100644 Quality/DeviceMenuItem.swift diff --git a/Quality.xcodeproj/project.pbxproj b/Quality.xcodeproj/project.pbxproj index 63be661..020d765 100644 --- a/Quality.xcodeproj/project.pbxproj +++ b/Quality.xcodeproj/project.pbxproj @@ -24,6 +24,7 @@ 127C972D281FCF000087313B /* AppVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 127C972C281FCF000087313B /* AppVersion.swift */; }; 1293436B28131591002E19A8 /* CurrentUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1293436A28131591002E19A8 /* CurrentUser.swift */; }; 12AFF5C12811AD40001CC6ED /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12AFF5C02811AD40001CC6ED /* AppDelegate.swift */; }; + 12F1AA572868639A006C1AD8 /* DeviceMenuItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12F1AA562868639A006C1AD8 /* DeviceMenuItem.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -44,6 +45,7 @@ 127C972C281FCF000087313B /* AppVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppVersion.swift; sourceTree = ""; }; 1293436A28131591002E19A8 /* CurrentUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentUser.swift; sourceTree = ""; }; 12AFF5C02811AD40001CC6ED /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 12F1AA562868639A006C1AD8 /* DeviceMenuItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceMenuItem.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -104,6 +106,7 @@ 1234F50D281E8F07007EC9F5 /* MediaTrack.swift */, 1234F50F281E9520007EC9F5 /* MediaRemoteController.swift */, 127C972C281FCF000087313B /* AppVersion.swift */, + 12F1AA562868639A006C1AD8 /* DeviceMenuItem.swift */, ); path = Quality; sourceTree = ""; @@ -199,6 +202,7 @@ 12AFF5C12811AD40001CC6ED /* AppDelegate.swift in Sources */, 1254A79C2813FB9400241107 /* Defaults.swift in Sources */, 1234F50E281E8F07007EC9F5 /* MediaTrack.swift in Sources */, + 12F1AA572868639A006C1AD8 /* DeviceMenuItem.swift in Sources */, 1221F3FB280F1EEF003E8B77 /* OutputDevices.swift in Sources */, 1272AAAE280DC68A00FD72BA /* CMPlayerStuff.swift in Sources */, 1272AAAC280DC5E900FD72BA /* Console.swift in Sources */, @@ -334,7 +338,7 @@ CODE_SIGN_ENTITLEMENTS = Quality/Quality.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 6; + CURRENT_PROJECT_VERSION = 7; DEVELOPMENT_ASSET_PATHS = "\"Quality/Preview Content\""; DEVELOPMENT_TEAM = 3X69W4AQD6; ENABLE_HARDENED_RUNTIME = YES; @@ -367,7 +371,7 @@ CODE_SIGN_ENTITLEMENTS = Quality/Quality.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 6; + CURRENT_PROJECT_VERSION = 7; DEVELOPMENT_ASSET_PATHS = "\"Quality/Preview Content\""; DEVELOPMENT_TEAM = 3X69W4AQD6; ENABLE_HARDENED_RUNTIME = YES; diff --git a/Quality/AppDelegate.swift b/Quality/AppDelegate.swift index e9e52e8..2dfab1f 100644 --- a/Quality/AppDelegate.swift +++ b/Quality/AppDelegate.swift @@ -6,7 +6,9 @@ // import Cocoa +import Combine import SwiftUI +import SimplyCoreAudio import PrivateMediaRemote class AppDelegate: NSObject, NSApplicationDelegate { @@ -16,9 +18,11 @@ class AppDelegate: NSObject, NSApplicationDelegate { private var outputDevices: OutputDevices! private let defaults = Defaults.shared private var mrController: MediaRemoteController! + private var devicesMenu: NSMenu! var statusItem: NSStatusItem? - + var cancellable: AnyCancellable? + private var _statusItemTitle = "Loading..." var statusItemTitle: String { get { @@ -71,6 +75,13 @@ class AppDelegate: NSObject, NSApplicationDelegate { let showSampleRateItem = NSMenuItem(title: defaults.statusBarItemTitle, action: #selector(toggleSampleRate(item:)), keyEquivalent: "") menu.addItem(showSampleRateItem) + let selectedDeviceItem = NSMenuItem(title: "Selected Device", action: nil, keyEquivalent: "") + self.devicesMenu = NSMenu() + selectedDeviceItem.submenu = self.devicesMenu + menu.addItem(selectedDeviceItem) + self.handleDevicesMenu() + + menu.addItem(NSMenuItem.separator()) let aboutItem = NSMenuItem(title: "About", action: nil, keyEquivalent: "") let versionItem = NSMenuItem(title: "Version - \(currentVersion)", action: nil, keyEquivalent: "") @@ -88,6 +99,36 @@ class AppDelegate: NSObject, NSApplicationDelegate { self.statusItem?.menu = menu self.statusItem?.button?.title = "Loading..." self.statusItemDisplay() + + cancellable = NotificationCenter.default.publisher(for: .deviceListChanged).sink(receiveValue: { _ in + self.handleDevicesMenu() + }) + + } + + func handleDevicesMenu() { + self.devicesMenu.removeAllItems() + let autoItem = DeviceMenuItem(title: "Default Device", action: #selector(deviceSelection(_:)), keyEquivalent: "", device: nil) + self.devicesMenu.addItem(autoItem) + autoItem.tag = -1 + autoItem.state = .on + outputDevices.selectedOutputDevice = nil + + var idx = 0 + for device in outputDevices.outputDevices { + let name = device.name + let item = DeviceMenuItem(title: name, action: #selector(deviceSelection(_:)), keyEquivalent: "", device: device) + item.tag = idx + item.state = .off + idx += 1 + self.devicesMenu.addItem(item) + } + } + + @objc func deviceSelection(_ sender: DeviceMenuItem) { + self.devicesMenu.items.forEach({$0.state = .off}) + sender.state = .on + outputDevices.selectedOutputDevice = sender.device } func statusItemDisplay() { diff --git a/Quality/ContentView.swift b/Quality/ContentView.swift index 27c12c8..67f0d4f 100644 --- a/Quality/ContentView.swift +++ b/Quality/ContentView.swift @@ -19,7 +19,7 @@ struct ContentView: View { Text(formattedSampleRate) .font(.system(size: 23, weight: .semibold, design: .default)) } - if let device = outputDevices.defaultOutputDevice { + if let device = outputDevices.selectedOutputDevice ?? outputDevices.defaultOutputDevice { Text(device.name) .font(.system(size: 14.5, weight: .regular, design: .default)) .foregroundColor(.secondary) diff --git a/Quality/DeviceMenuItem.swift b/Quality/DeviceMenuItem.swift new file mode 100644 index 0000000..5c5ef3f --- /dev/null +++ b/Quality/DeviceMenuItem.swift @@ -0,0 +1,22 @@ +// +// DeviceMenuItem.swift +// LosslessSwitcher +// +// Created by Vincent Neo on 26/6/22. +// + +import Cocoa +import SimplyCoreAudio + +class DeviceMenuItem: NSMenuItem { + var device: AudioDevice? + + init(title string: String, action selector: Selector?, keyEquivalent charCode: String, device: AudioDevice? = nil) { + self.device = device + super.init(title: string, action: selector, keyEquivalent: charCode) + } + + required init(coder: NSCoder) { + super.init(coder: coder) + } +} diff --git a/Quality/OutputDevices.swift b/Quality/OutputDevices.swift index 786b6ae..93b6fe0 100644 --- a/Quality/OutputDevices.swift +++ b/Quality/OutputDevices.swift @@ -10,6 +10,7 @@ import Foundation import SimplyCoreAudio class OutputDevices: ObservableObject { + @Published var selectedOutputDevice: AudioDevice? // auto if nil @Published var defaultOutputDevice: AudioDevice? @Published var outputDevices = [AudioDevice]() @Published var currentSampleRate: Float64? @@ -19,6 +20,8 @@ class OutputDevices: ObservableObject { private var changesCancellable: AnyCancellable? private var defaultChangesCancellable: AnyCancellable? private var timerCancellable: AnyCancellable? + private var outputSelectionCancellable: AnyCancellable? + private var consoleQueue = DispatchQueue(label: "consoleQueue", qos: .userInteractive) private var previousSampleRate: Float64? @@ -45,6 +48,10 @@ class OutputDevices: ObservableObject { self.getDeviceSampleRate() }) + outputSelectionCancellable = selectedOutputDevice.publisher.sink(receiveValue: { _ in + self.getDeviceSampleRate() + }) + } deinit { @@ -60,7 +67,6 @@ class OutputDevices: ObservableObject { .publish(every: 2, on: .main, in: .default) .autoconnect() .sink { _ in - print("cancellable \(self.timerCancellable), times \(self.timerCalls)") if self.timerCalls == 5 { self.timerCalls = 0 self.timerCancellable?.cancel() @@ -76,7 +82,7 @@ class OutputDevices: ObservableObject { } func getDeviceSampleRate() { - let defaultDevice = self.defaultOutputDevice + let defaultDevice = self.selectedOutputDevice ?? self.defaultOutputDevice guard let sampleRate = defaultDevice?.nominalSampleRate else { return } self.updateSampleRate(sampleRate) } @@ -93,7 +99,7 @@ class OutputDevices: ObservableObject { allStats.sort(by: {$0.priority > $1.priority}) print(allStats) - let defaultDevice = self.defaultOutputDevice + let defaultDevice = self.selectedOutputDevice ?? self.defaultOutputDevice if let first = allStats.first, let supported = defaultDevice?.nominalSampleRates { let sampleRate = Float64(first.sampleRate) From 79fd115b3c7db6c521bbd03ee6f034269703521a Mon Sep 17 00:00:00 2001 From: Vincent Neo <23420208+vincentneo@users.noreply.github.com> Date: Fri, 2 Sep 2022 01:07:24 +0800 Subject: [PATCH 06/32] Local file sample rate detection via AppleScript method provided by @fjolnir in #15. should fix #15, untested. --- Quality/Info.plist | 2 ++ Quality/OutputDevices.swift | 42 ++++++++++++++++++++++++++++++------ Quality/Quality.entitlements | 5 ++++- 3 files changed, 42 insertions(+), 7 deletions(-) diff --git a/Quality/Info.plist b/Quality/Info.plist index 6f7ae58..deaca6f 100644 --- a/Quality/Info.plist +++ b/Quality/Info.plist @@ -4,5 +4,7 @@ LSUIElement + NSAppleEventsUsageDescription + This permission is required for local file sample rate detection. diff --git a/Quality/OutputDevices.swift b/Quality/OutputDevices.swift index 93b6fe0..8372fed 100644 --- a/Quality/OutputDevices.swift +++ b/Quality/OutputDevices.swift @@ -87,15 +87,45 @@ class OutputDevices: ObservableObject { self.updateSampleRate(sampleRate) } + func getSampleRateFromAppleScript() -> Double? { + let scriptContents = "tell application \"Music\" to get sample rate of current track" + var error: NSDictionary? + + if let script = NSAppleScript(source: scriptContents) { + let output = script.executeAndReturnError(&error).stringValue + + if let error = error { + print("[APPLESCRIPT] - \(error)") + } + guard let output = output else { return nil } + + if output == "missing value" { + return nil + } + else { + return Double(output) + } + } + + return nil + } + func switchLatestSampleRate(recursion: Bool = false) { do { var allStats = [CMPlayerStats]() - let musicLogs = try Console.getRecentEntries(type: .music) - //let coreAudioLogs = try Console.getRecentEntries(type: .coreAudio) - let coreMediaLogs = try Console.getRecentEntries(type: .coreMedia) - allStats.append(contentsOf: CMPlayerParser.parseMusicConsoleLogs(musicLogs)) - //allStats.append(contentsOf: CMPlayerParser.parseCoreAudioConsoleLogs(coreAudioLogs)) - allStats.append(contentsOf: CMPlayerParser.parseCoreMediaConsoleLogs(coreMediaLogs)) + let appleScriptRate = getSampleRateFromAppleScript() + + if let appleScriptRate = appleScriptRate { + allStats.append(CMPlayerStats(sampleRate: appleScriptRate, bitDepth: 0, date: .init(), priority: 100)) + } + else { + let musicLogs = try Console.getRecentEntries(type: .music) + //let coreAudioLogs = try Console.getRecentEntries(type: .coreAudio) + let coreMediaLogs = try Console.getRecentEntries(type: .coreMedia) + allStats.append(contentsOf: CMPlayerParser.parseMusicConsoleLogs(musicLogs)) + //allStats.append(contentsOf: CMPlayerParser.parseCoreAudioConsoleLogs(coreAudioLogs)) + allStats.append(contentsOf: CMPlayerParser.parseCoreMediaConsoleLogs(coreMediaLogs)) + } allStats.sort(by: {$0.priority > $1.priority}) print(allStats) diff --git a/Quality/Quality.entitlements b/Quality/Quality.entitlements index 0c67376..49ad0bb 100644 --- a/Quality/Quality.entitlements +++ b/Quality/Quality.entitlements @@ -1,5 +1,8 @@ - + + com.apple.security.automation.apple-events + + From e7aedc982fecec134d6cb01071573f1ac1b486bc Mon Sep 17 00:00:00 2001 From: Vincent Neo <23420208+vincentneo@users.noreply.github.com> Date: Fri, 2 Sep 2022 01:08:32 +0800 Subject: [PATCH 07/32] increment build number --- Quality.xcodeproj/project.pbxproj | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Quality.xcodeproj/project.pbxproj b/Quality.xcodeproj/project.pbxproj index 020d765..ff1e64d 100644 --- a/Quality.xcodeproj/project.pbxproj +++ b/Quality.xcodeproj/project.pbxproj @@ -338,13 +338,14 @@ CODE_SIGN_ENTITLEMENTS = Quality/Quality.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 7; + CURRENT_PROJECT_VERSION = 8; DEVELOPMENT_ASSET_PATHS = "\"Quality/Preview Content\""; DEVELOPMENT_TEAM = 3X69W4AQD6; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Quality/Info.plist; + INFOPLIST_KEY_NSAppleEventsUsageDescription = "This permission is required for local file sample rate detection."; INFOPLIST_KEY_NSHumanReadableCopyright = "2022 Vincent Neo"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -371,13 +372,14 @@ CODE_SIGN_ENTITLEMENTS = Quality/Quality.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 7; + CURRENT_PROJECT_VERSION = 8; DEVELOPMENT_ASSET_PATHS = "\"Quality/Preview Content\""; DEVELOPMENT_TEAM = 3X69W4AQD6; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Quality/Info.plist; + INFOPLIST_KEY_NSAppleEventsUsageDescription = "This permission is required for local file sample rate detection."; INFOPLIST_KEY_NSHumanReadableCopyright = "2022 Vincent Neo"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", From ac3368842c4fac660f7ada2bc5fb7188ebddfe9a Mon Sep 17 00:00:00 2001 From: Vincent Neo <23420208+vincentneo@users.noreply.github.com> Date: Wed, 16 Nov 2022 23:53:41 +0800 Subject: [PATCH 08/32] build 9, attempt to fix #32 Test implementation. Explicitly select the device that you want to playback (not the auto "default device" selection). Check if device will continue to stick to your selected device, even in circumstances such as EQ / virtual device. App device selection menu may glitch out showing 2 checkmarks on app restart, but that is expected, cosmetic behaviour. --- Quality.xcodeproj/project.pbxproj | 4 ++-- Quality/AppDelegate.swift | 9 ++++++++- Quality/Defaults.swift | 10 ++++++++++ 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/Quality.xcodeproj/project.pbxproj b/Quality.xcodeproj/project.pbxproj index ff1e64d..78c5f3a 100644 --- a/Quality.xcodeproj/project.pbxproj +++ b/Quality.xcodeproj/project.pbxproj @@ -338,7 +338,7 @@ CODE_SIGN_ENTITLEMENTS = Quality/Quality.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 8; + CURRENT_PROJECT_VERSION = 9; DEVELOPMENT_ASSET_PATHS = "\"Quality/Preview Content\""; DEVELOPMENT_TEAM = 3X69W4AQD6; ENABLE_HARDENED_RUNTIME = YES; @@ -372,7 +372,7 @@ CODE_SIGN_ENTITLEMENTS = Quality/Quality.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 8; + CURRENT_PROJECT_VERSION = 9; DEVELOPMENT_ASSET_PATHS = "\"Quality/Preview Content\""; DEVELOPMENT_TEAM = 3X69W4AQD6; ENABLE_HARDENED_RUNTIME = YES; diff --git a/Quality/AppDelegate.swift b/Quality/AppDelegate.swift index 2dfab1f..a2b9848 100644 --- a/Quality/AppDelegate.swift +++ b/Quality/AppDelegate.swift @@ -116,10 +116,16 @@ class AppDelegate: NSObject, NSApplicationDelegate { var idx = 0 for device in outputDevices.outputDevices { + let uid = device.uid let name = device.name let item = DeviceMenuItem(title: name, action: #selector(deviceSelection(_:)), keyEquivalent: "", device: device) item.tag = idx - item.state = .off + if let uid, uid == Defaults.shared.selectedDeviceUID { + item.state = .on + } + else { + item.state = .off + } idx += 1 self.devicesMenu.addItem(item) } @@ -129,6 +135,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { self.devicesMenu.items.forEach({$0.state = .off}) sender.state = .on outputDevices.selectedOutputDevice = sender.device + Defaults.shared.selectedDeviceUID = sender.device?.uid } func statusItemDisplay() { diff --git a/Quality/Defaults.swift b/Quality/Defaults.swift index d7a8817..c146a2b 100644 --- a/Quality/Defaults.swift +++ b/Quality/Defaults.swift @@ -10,6 +10,7 @@ import Foundation class Defaults { static let shared = Defaults() private let kUserPreferIconStatusBarItem = "com.vincent-neo.LosslessSwitcher-Key-UserPreferIconStatusBarItem" + private let kSelectedDeviceUID = "com.vincent-neo.LosslessSwitcher-Key-SelectedDeviceUID" private init() { UserDefaults.standard.register(defaults: [kUserPreferIconStatusBarItem : true]) @@ -24,6 +25,15 @@ class Defaults { } } + var selectedDeviceUID: String? { + get { + return UserDefaults.standard.string(forKey: kSelectedDeviceUID) + } + set { + UserDefaults.standard.set(newValue, forKey: kSelectedDeviceUID) + } + } + var statusBarItemTitle: String { let title = self.userPreferIconStatusBarItem ? "Show Sample Rate" : "Show Icon" return title From 5540fe4490c23dde061b899d2ebd4dbe676759df Mon Sep 17 00:00:00 2001 From: Vincent Neo <23420208+vincentneo@users.noreply.github.com> Date: Thu, 17 Nov 2022 00:23:29 +0800 Subject: [PATCH 09/32] crudely disable applescript --- Quality.xcodeproj/project.pbxproj | 4 ++-- Quality/OutputDevices.swift | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Quality.xcodeproj/project.pbxproj b/Quality.xcodeproj/project.pbxproj index 78c5f3a..67105c1 100644 --- a/Quality.xcodeproj/project.pbxproj +++ b/Quality.xcodeproj/project.pbxproj @@ -338,7 +338,7 @@ CODE_SIGN_ENTITLEMENTS = Quality/Quality.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 9; + CURRENT_PROJECT_VERSION = 10; DEVELOPMENT_ASSET_PATHS = "\"Quality/Preview Content\""; DEVELOPMENT_TEAM = 3X69W4AQD6; ENABLE_HARDENED_RUNTIME = YES; @@ -372,7 +372,7 @@ CODE_SIGN_ENTITLEMENTS = Quality/Quality.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 9; + CURRENT_PROJECT_VERSION = 10; DEVELOPMENT_ASSET_PATHS = "\"Quality/Preview Content\""; DEVELOPMENT_TEAM = 3X69W4AQD6; ENABLE_HARDENED_RUNTIME = YES; diff --git a/Quality/OutputDevices.swift b/Quality/OutputDevices.swift index 8372fed..e1c139a 100644 --- a/Quality/OutputDevices.swift +++ b/Quality/OutputDevices.swift @@ -113,19 +113,19 @@ class OutputDevices: ObservableObject { func switchLatestSampleRate(recursion: Bool = false) { do { var allStats = [CMPlayerStats]() - let appleScriptRate = getSampleRateFromAppleScript() - - if let appleScriptRate = appleScriptRate { - allStats.append(CMPlayerStats(sampleRate: appleScriptRate, bitDepth: 0, date: .init(), priority: 100)) - } - else { +// let appleScriptRate = getSampleRateFromAppleScript() +// +// if let appleScriptRate = appleScriptRate { +// allStats.append(CMPlayerStats(sampleRate: appleScriptRate, bitDepth: 0, date: .init(), priority: 100)) +// } +// else { let musicLogs = try Console.getRecentEntries(type: .music) //let coreAudioLogs = try Console.getRecentEntries(type: .coreAudio) let coreMediaLogs = try Console.getRecentEntries(type: .coreMedia) allStats.append(contentsOf: CMPlayerParser.parseMusicConsoleLogs(musicLogs)) //allStats.append(contentsOf: CMPlayerParser.parseCoreAudioConsoleLogs(coreAudioLogs)) allStats.append(contentsOf: CMPlayerParser.parseCoreMediaConsoleLogs(coreMediaLogs)) - } + //} allStats.sort(by: {$0.priority > $1.priority}) print(allStats) From 2692dcad7ad3261139bd643cffd806ccda34fe49 Mon Sep 17 00:00:00 2001 From: Vincent Neo <23420208+vincentneo@users.noreply.github.com> Date: Thu, 17 Nov 2022 12:23:56 +0800 Subject: [PATCH 10/32] build 11:fix cosmetic issue,set device to selected --- Quality.xcodeproj/project.pbxproj | 4 ++-- Quality/AppDelegate.swift | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Quality.xcodeproj/project.pbxproj b/Quality.xcodeproj/project.pbxproj index 78c5f3a..a9a526e 100644 --- a/Quality.xcodeproj/project.pbxproj +++ b/Quality.xcodeproj/project.pbxproj @@ -338,7 +338,7 @@ CODE_SIGN_ENTITLEMENTS = Quality/Quality.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 9; + CURRENT_PROJECT_VERSION = 11; DEVELOPMENT_ASSET_PATHS = "\"Quality/Preview Content\""; DEVELOPMENT_TEAM = 3X69W4AQD6; ENABLE_HARDENED_RUNTIME = YES; @@ -372,7 +372,7 @@ CODE_SIGN_ENTITLEMENTS = Quality/Quality.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 9; + CURRENT_PROJECT_VERSION = 11; DEVELOPMENT_ASSET_PATHS = "\"Quality/Preview Content\""; DEVELOPMENT_TEAM = 3X69W4AQD6; ENABLE_HARDENED_RUNTIME = YES; diff --git a/Quality/AppDelegate.swift b/Quality/AppDelegate.swift index a2b9848..732b4bf 100644 --- a/Quality/AppDelegate.swift +++ b/Quality/AppDelegate.swift @@ -111,7 +111,9 @@ class AppDelegate: NSObject, NSApplicationDelegate { let autoItem = DeviceMenuItem(title: "Default Device", action: #selector(deviceSelection(_:)), keyEquivalent: "", device: nil) self.devicesMenu.addItem(autoItem) autoItem.tag = -1 - autoItem.state = .on + if Defaults.shared.selectedDeviceUID == nil { + autoItem.state = .on + } outputDevices.selectedOutputDevice = nil var idx = 0 @@ -122,6 +124,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { item.tag = idx if let uid, uid == Defaults.shared.selectedDeviceUID { item.state = .on + outputDevices.selectedOutputDevice = device } else { item.state = .off From 346d9b2cdbdd7b070627f8dbbab7267c41c7863c Mon Sep 17 00:00:00 2001 From: Vincent Neo <23420208+vincentneo@users.noreply.github.com> Date: Thu, 17 Nov 2022 12:32:31 +0800 Subject: [PATCH 11/32] Revert "Merge branch 'applescript-disabled' into improvements-1" This reverts commit 7dff2f82e936d921e0b58e89f2327351d56a3357, reversing changes made to 2692dcad7ad3261139bd643cffd806ccda34fe49. --- Quality/OutputDevices.swift | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Quality/OutputDevices.swift b/Quality/OutputDevices.swift index e1c139a..8372fed 100644 --- a/Quality/OutputDevices.swift +++ b/Quality/OutputDevices.swift @@ -113,19 +113,19 @@ class OutputDevices: ObservableObject { func switchLatestSampleRate(recursion: Bool = false) { do { var allStats = [CMPlayerStats]() -// let appleScriptRate = getSampleRateFromAppleScript() -// -// if let appleScriptRate = appleScriptRate { -// allStats.append(CMPlayerStats(sampleRate: appleScriptRate, bitDepth: 0, date: .init(), priority: 100)) -// } -// else { + let appleScriptRate = getSampleRateFromAppleScript() + + if let appleScriptRate = appleScriptRate { + allStats.append(CMPlayerStats(sampleRate: appleScriptRate, bitDepth: 0, date: .init(), priority: 100)) + } + else { let musicLogs = try Console.getRecentEntries(type: .music) //let coreAudioLogs = try Console.getRecentEntries(type: .coreAudio) let coreMediaLogs = try Console.getRecentEntries(type: .coreMedia) allStats.append(contentsOf: CMPlayerParser.parseMusicConsoleLogs(musicLogs)) //allStats.append(contentsOf: CMPlayerParser.parseCoreAudioConsoleLogs(coreAudioLogs)) allStats.append(contentsOf: CMPlayerParser.parseCoreMediaConsoleLogs(coreMediaLogs)) - //} + } allStats.sort(by: {$0.priority > $1.priority}) print(allStats) From b3f5e77903c5ecd5e5498e42055ad478616a4f14 Mon Sep 17 00:00:00 2001 From: Vincent Neo <23420208+vincentneo@users.noreply.github.com> Date: Thu, 17 Nov 2022 12:38:33 +0800 Subject: [PATCH 12/32] fix auto device not selected if prev was removed --- Quality/AppDelegate.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Quality/AppDelegate.swift b/Quality/AppDelegate.swift index 732b4bf..64ee547 100644 --- a/Quality/AppDelegate.swift +++ b/Quality/AppDelegate.swift @@ -111,13 +111,15 @@ class AppDelegate: NSObject, NSApplicationDelegate { let autoItem = DeviceMenuItem(title: "Default Device", action: #selector(deviceSelection(_:)), keyEquivalent: "", device: nil) self.devicesMenu.addItem(autoItem) autoItem.tag = -1 - if Defaults.shared.selectedDeviceUID == nil { + let selectedUID = Defaults.shared.selectedDeviceUID + if selectedUID == nil || (selectedUID != nil && !self.doesDeviceUID(selectedUID, existsIn: outputDevices.outputDevices)) { autoItem.state = .on } outputDevices.selectedOutputDevice = nil var idx = 0 for device in outputDevices.outputDevices { + let uid = device.uid let name = device.name let item = DeviceMenuItem(title: name, action: #selector(deviceSelection(_:)), keyEquivalent: "", device: device) @@ -134,6 +136,10 @@ class AppDelegate: NSObject, NSApplicationDelegate { } } + private func doesDeviceUID(_ uid: String?, existsIn outputDevices: [AudioDevice]) -> Bool { + return !outputDevices.filter({$0.uid == uid}).isEmpty + } + @objc func deviceSelection(_ sender: DeviceMenuItem) { self.devicesMenu.items.forEach({$0.state = .off}) sender.state = .on From 5aad3bf41b6413ee7370c5f660aad31035eea828 Mon Sep 17 00:00:00 2001 From: Vincent Neo <23420208+vincentneo@users.noreply.github.com> Date: Mon, 28 Nov 2022 11:19:50 +0800 Subject: [PATCH 13/32] Create .gitignore --- .gitignore | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f92066c --- /dev/null +++ b/.gitignore @@ -0,0 +1,54 @@ +# Created by https://www.toptal.com/developers/gitignore/api/macos,xcode +# Edit at https://www.toptal.com/developers/gitignore?templates=macos,xcode + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### macOS Patch ### +# iCloud generated files +*.icloud + +### Xcode ### +## User settings +xcuserdata/ + +## Xcode 8 and earlier +*.xcscmblueprint +*.xccheckout + +### Xcode Patch ### +*.xcodeproj/* +!*.xcodeproj/project.pbxproj +!*.xcodeproj/xcshareddata/ +!*.xcodeproj/project.xcworkspace/ +!*.xcworkspace/contents.xcworkspacedata +/*.gcno +**/xcshareddata/WorkspaceSettings.xcsettings + +# End of https://www.toptal.com/developers/gitignore/api/macos,xcode From 402e19e27edd2167ee6f85ea36ef8271d25f61f8 Mon Sep 17 00:00:00 2001 From: Vincent Neo <23420208+vincentneo@users.noreply.github.com> Date: Mon, 28 Nov 2022 11:20:23 +0800 Subject: [PATCH 14/32] restrict to Music app process attempt fix #51 --- Quality/Console.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Quality/Console.swift b/Quality/Console.swift index d7092fa..f87bdd0 100644 --- a/Quality/Console.swift +++ b/Quality/Console.swift @@ -20,7 +20,7 @@ enum EntryType: String { case coreMedia = "com.apple.coremedia" var predicate: NSPredicate { - NSPredicate(format: "subsystem = %@", rawValue) + NSPredicate(format: "(subsystem = %@) AND (process = %@)", argumentArray: [rawValue, "Music"]) } } From a828c9ba0c0c8db6067cba24308292ce90c12a68 Mon Sep 17 00:00:00 2001 From: Vincent Neo <23420208+vincentneo@users.noreply.github.com> Date: Mon, 28 Nov 2022 11:21:08 +0800 Subject: [PATCH 15/32] disable applescript with a more convenient boolean --- Quality/OutputDevices.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Quality/OutputDevices.swift b/Quality/OutputDevices.swift index 8372fed..bee33b7 100644 --- a/Quality/OutputDevices.swift +++ b/Quality/OutputDevices.swift @@ -113,9 +113,11 @@ class OutputDevices: ObservableObject { func switchLatestSampleRate(recursion: Bool = false) { do { var allStats = [CMPlayerStats]() - let appleScriptRate = getSampleRateFromAppleScript() + let enableAppleScript = false // TODO: Do something about it - if let appleScriptRate = appleScriptRate { + let appleScriptRate = getSampleRateFromAppleScript() + + if enableAppleScript, let appleScriptRate = appleScriptRate { allStats.append(CMPlayerStats(sampleRate: appleScriptRate, bitDepth: 0, date: .init(), priority: 100)) } else { From 7564aba22c096f700522357e157242d5490475b7 Mon Sep 17 00:00:00 2001 From: Vincent Neo <23420208+vincentneo@users.noreply.github.com> Date: Mon, 28 Nov 2022 11:27:20 +0800 Subject: [PATCH 16/32] ui multiline tweak if device name is too long it should align center --- Quality/ContentView.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Quality/ContentView.swift b/Quality/ContentView.swift index 67f0d4f..cc047b5 100644 --- a/Quality/ContentView.swift +++ b/Quality/ContentView.swift @@ -23,6 +23,7 @@ struct ContentView: View { Text(device.name) .font(.system(size: 14.5, weight: .regular, design: .default)) .foregroundColor(.secondary) + .multilineTextAlignment(.center) } } } From 0407c535f36b9b2b39877c9d31e1e250a79986a1 Mon Sep 17 00:00:00 2001 From: Vincent Neo <23420208+vincentneo@users.noreply.github.com> Date: Mon, 28 Nov 2022 11:55:39 +0800 Subject: [PATCH 17/32] on off toggle for applescript partial fix for #30, as user may just disable option if required. --- Quality/AppDelegate.swift | 10 ++++++++++ Quality/Defaults.swift | 15 ++++++++++++++- Quality/OutputDevices.swift | 4 +++- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/Quality/AppDelegate.swift b/Quality/AppDelegate.swift index 64ee547..3cd1f01 100644 --- a/Quality/AppDelegate.swift +++ b/Quality/AppDelegate.swift @@ -75,6 +75,10 @@ class AppDelegate: NSObject, NSApplicationDelegate { let showSampleRateItem = NSMenuItem(title: defaults.statusBarItemTitle, action: #selector(toggleSampleRate(item:)), keyEquivalent: "") menu.addItem(showSampleRateItem) + let useAppleScriptItem = NSMenuItem(title: "Detect Local Track Rates", action: #selector(toggleAppleScript(item:)), keyEquivalent: "") + menu.addItem(useAppleScriptItem) + useAppleScriptItem.state = Defaults.shared.userPreferAppleScript ? .on : .off + let selectedDeviceItem = NSMenuItem(title: "Selected Device", action: nil, keyEquivalent: "") self.devicesMenu = NSMenu() selectedDeviceItem.submenu = self.devicesMenu @@ -164,4 +168,10 @@ class AppDelegate: NSObject, NSApplicationDelegate { item.title = defaults.statusBarItemTitle } + @objc func toggleAppleScript(item: NSMenuItem) { + defaults.userPreferAppleScript = !defaults.userPreferAppleScript + outputDevices.enableAppleScript = defaults.userPreferAppleScript + item.state = defaults.userPreferAppleScript ? .on : .off + } + } diff --git a/Quality/Defaults.swift b/Quality/Defaults.swift index c146a2b..ad5162d 100644 --- a/Quality/Defaults.swift +++ b/Quality/Defaults.swift @@ -10,10 +10,14 @@ import Foundation class Defaults { static let shared = Defaults() private let kUserPreferIconStatusBarItem = "com.vincent-neo.LosslessSwitcher-Key-UserPreferIconStatusBarItem" + private let kUserPreferAppleScript = "com.vincent-neo.LosslessSwitcher-Key-UserPreferAppleScript" private let kSelectedDeviceUID = "com.vincent-neo.LosslessSwitcher-Key-SelectedDeviceUID" private init() { - UserDefaults.standard.register(defaults: [kUserPreferIconStatusBarItem : true]) + UserDefaults.standard.register(defaults: [ + kUserPreferIconStatusBarItem : true, + kUserPreferAppleScript : true + ]) } var userPreferIconStatusBarItem: Bool { @@ -25,6 +29,15 @@ class Defaults { } } + var userPreferAppleScript: Bool { + get { + return UserDefaults.standard.bool(forKey: kUserPreferAppleScript) + } + set { + UserDefaults.standard.set(newValue, forKey: kUserPreferAppleScript) + } + } + var selectedDeviceUID: String? { get { return UserDefaults.standard.string(forKey: kSelectedDeviceUID) diff --git a/Quality/OutputDevices.swift b/Quality/OutputDevices.swift index bee33b7..79adc52 100644 --- a/Quality/OutputDevices.swift +++ b/Quality/OutputDevices.swift @@ -15,6 +15,8 @@ class OutputDevices: ObservableObject { @Published var outputDevices = [AudioDevice]() @Published var currentSampleRate: Float64? + var enableAppleScript = Defaults.shared.userPreferAppleScript + private let coreAudio = SimplyCoreAudio() private var changesCancellable: AnyCancellable? @@ -113,11 +115,11 @@ class OutputDevices: ObservableObject { func switchLatestSampleRate(recursion: Bool = false) { do { var allStats = [CMPlayerStats]() - let enableAppleScript = false // TODO: Do something about it let appleScriptRate = getSampleRateFromAppleScript() if enableAppleScript, let appleScriptRate = appleScriptRate { + print("AppleScript ran") allStats.append(CMPlayerStats(sampleRate: appleScriptRate, bitDepth: 0, date: .init(), priority: 100)) } else { From 22bc619549ffe704bed9b4664bddd1fe9f95260f Mon Sep 17 00:00:00 2001 From: Vincent Neo <23420208+vincentneo@users.noreply.github.com> Date: Mon, 28 Nov 2022 11:55:57 +0800 Subject: [PATCH 18/32] increase build number to 12 --- Quality.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Quality.xcodeproj/project.pbxproj b/Quality.xcodeproj/project.pbxproj index a9a526e..c1d24f7 100644 --- a/Quality.xcodeproj/project.pbxproj +++ b/Quality.xcodeproj/project.pbxproj @@ -338,7 +338,7 @@ CODE_SIGN_ENTITLEMENTS = Quality/Quality.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 11; + CURRENT_PROJECT_VERSION = 12; DEVELOPMENT_ASSET_PATHS = "\"Quality/Preview Content\""; DEVELOPMENT_TEAM = 3X69W4AQD6; ENABLE_HARDENED_RUNTIME = YES; @@ -372,7 +372,7 @@ CODE_SIGN_ENTITLEMENTS = Quality/Quality.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 11; + CURRENT_PROJECT_VERSION = 12; DEVELOPMENT_ASSET_PATHS = "\"Quality/Preview Content\""; DEVELOPMENT_TEAM = 3X69W4AQD6; ENABLE_HARDENED_RUNTIME = YES; From 1fd40873bab517ec5005d8473c9e6e3e4abed4a2 Mon Sep 17 00:00:00 2001 From: Vincent Neo <23420208+vincentneo@users.noreply.github.com> Date: Tue, 3 Jan 2023 17:34:15 +0800 Subject: [PATCH 19/32] support bit depth switching --- Quality.xcodeproj/project.pbxproj | 4 + ...udioStreamBasicDescription+Equatable.swift | 14 ++ Quality/CMPlayerStuff.swift | 11 +- Quality/OutputDevices.swift | 190 ++++++++++++------ 4 files changed, 158 insertions(+), 61 deletions(-) create mode 100644 Quality/AudioStreamBasicDescription+Equatable.swift diff --git a/Quality.xcodeproj/project.pbxproj b/Quality.xcodeproj/project.pbxproj index c1d24f7..13280b4 100644 --- a/Quality.xcodeproj/project.pbxproj +++ b/Quality.xcodeproj/project.pbxproj @@ -25,6 +25,7 @@ 1293436B28131591002E19A8 /* CurrentUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1293436A28131591002E19A8 /* CurrentUser.swift */; }; 12AFF5C12811AD40001CC6ED /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12AFF5C02811AD40001CC6ED /* AppDelegate.swift */; }; 12F1AA572868639A006C1AD8 /* DeviceMenuItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12F1AA562868639A006C1AD8 /* DeviceMenuItem.swift */; }; + BF7E0D09296336DA009FFEEC /* AudioStreamBasicDescription+Equatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF7E0D08296336DA009FFEEC /* AudioStreamBasicDescription+Equatable.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -46,6 +47,7 @@ 1293436A28131591002E19A8 /* CurrentUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentUser.swift; sourceTree = ""; }; 12AFF5C02811AD40001CC6ED /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 12F1AA562868639A006C1AD8 /* DeviceMenuItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceMenuItem.swift; sourceTree = ""; }; + BF7E0D08296336DA009FFEEC /* AudioStreamBasicDescription+Equatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AudioStreamBasicDescription+Equatable.swift"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -107,6 +109,7 @@ 1234F50F281E9520007EC9F5 /* MediaRemoteController.swift */, 127C972C281FCF000087313B /* AppVersion.swift */, 12F1AA562868639A006C1AD8 /* DeviceMenuItem.swift */, + BF7E0D08296336DA009FFEEC /* AudioStreamBasicDescription+Equatable.swift */, ); path = Quality; sourceTree = ""; @@ -201,6 +204,7 @@ files = ( 12AFF5C12811AD40001CC6ED /* AppDelegate.swift in Sources */, 1254A79C2813FB9400241107 /* Defaults.swift in Sources */, + BF7E0D09296336DA009FFEEC /* AudioStreamBasicDescription+Equatable.swift in Sources */, 1234F50E281E8F07007EC9F5 /* MediaTrack.swift in Sources */, 12F1AA572868639A006C1AD8 /* DeviceMenuItem.swift in Sources */, 1221F3FB280F1EEF003E8B77 /* OutputDevices.swift in Sources */, diff --git a/Quality/AudioStreamBasicDescription+Equatable.swift b/Quality/AudioStreamBasicDescription+Equatable.swift new file mode 100644 index 0000000..0bfc44e --- /dev/null +++ b/Quality/AudioStreamBasicDescription+Equatable.swift @@ -0,0 +1,14 @@ +// +// AudioStreamBasicDescription+Equatable.swift +// LosslessSwitcher +// +// Created by Vincent Neo on 2/1/23. +// + +import CoreAudioTypes + +extension AudioStreamBasicDescription: Equatable { + public static func == (lhs: AudioStreamBasicDescription, rhs: AudioStreamBasicDescription) -> Bool { + return lhs.mSampleRate == rhs.mSampleRate && lhs.mBitsPerChannel == rhs.mBitsPerChannel + } +} diff --git a/Quality/CMPlayerStuff.swift b/Quality/CMPlayerStuff.swift index 73a4033..7c63eb7 100644 --- a/Quality/CMPlayerStuff.swift +++ b/Quality/CMPlayerStuff.swift @@ -80,13 +80,16 @@ class CMPlayerParser { bitDepth = nil } - if rawMessage.contains("Input format: ") { + if rawMessage.contains("ACAppleLosslessDecoder") && rawMessage.contains("Input format: ") { if let subSampleRate = rawMessage.firstSubstring(between: "ch, ", and: " Hz") { let strSampleRate = String(subSampleRate) sampleRate = Double(strSampleRate) } - bitDepth = 24 // not important anymore, just putting as placeholder, at least until there's a way to set bit depth with Core Audio. + if let subBitDepth = rawMessage.firstSubstring(between: "from ", and: "-bit source") { + let strBitDepth = String(subBitDepth) + bitDepth = Int(strBitDepth) + } } if let sr = sampleRate, @@ -109,7 +112,7 @@ class CMPlayerParser { let kTimeDifferenceAcceptance = 5.0 // seconds var lastDate: Date? var sampleRate: Double? - let bitDepth = 24 // not important anymore, just putting as placeholder, at least until there's a way to set bit depth with Core Audio. + let bitDepth = 24 // Core Media don't provide bit depth, but I am keeping this for now, since it seems to be the first to deliver accurate bitrate data, fairly consistently. var stats = [CMPlayerStats]() @@ -129,7 +132,7 @@ class CMPlayerParser { } if let sr = sampleRate { - let stat = CMPlayerStats(sampleRate: sr, bitDepth: bitDepth, date: date, priority: 10) + let stat = CMPlayerStats(sampleRate: sr, bitDepth: bitDepth, date: date, priority: 2) stats.append(stat) sampleRate = nil print("detected stat \(stat)") diff --git a/Quality/OutputDevices.swift b/Quality/OutputDevices.swift index 79adc52..4cac261 100644 --- a/Quality/OutputDevices.swift +++ b/Quality/OutputDevices.swift @@ -8,6 +8,7 @@ import Combine import Foundation import SimplyCoreAudio +import CoreAudioTypes class OutputDevices: ObservableObject { @Published var selectedOutputDevice: AudioDevice? // auto if nil @@ -112,80 +113,155 @@ class OutputDevices: ObservableObject { return nil } - func switchLatestSampleRate(recursion: Bool = false) { - do { - var allStats = [CMPlayerStats]() - - let appleScriptRate = getSampleRateFromAppleScript() + let appleScriptQueue = DispatchQueue(label: "AppleScriptQueue") + + func getSampleRateFromAppleScript(_ completion: @escaping (Double?) -> ()) { + let scriptContents = "tell application \"Music\" to get sample rate of current track" + var error: NSDictionary? + + self.appleScriptQueue.async { + if let script = NSAppleScript(source: scriptContents) { + let output = script.executeAndReturnError(&error).stringValue + var dOutput: Double? + + defer { + completion(dOutput) + } + + if let error = error { + print("[APPLESCRIPT] - \(error)") + } + guard let output = output else { return } - if enableAppleScript, let appleScriptRate = appleScriptRate { - print("AppleScript ran") - allStats.append(CMPlayerStats(sampleRate: appleScriptRate, bitDepth: 0, date: .init(), priority: 100)) + if output == "missing value" { + return + } + else { + dOutput = Double(output) + } + } + } + } + + func getAllStats() -> [CMPlayerStats] { + var allStats = [CMPlayerStats]() + + do { + if enableAppleScript { + if let appleScriptRate = getSampleRateFromAppleScript() { + print("AppleScript ran") + allStats.append(CMPlayerStats(sampleRate: appleScriptRate, bitDepth: 0, date: .init(), priority: 100)) + } } else { let musicLogs = try Console.getRecentEntries(type: .music) - //let coreAudioLogs = try Console.getRecentEntries(type: .coreAudio) - let coreMediaLogs = try Console.getRecentEntries(type: .coreMedia) + let coreAudioLogs = try Console.getRecentEntries(type: .coreAudio) + //let coreMediaLogs = try Console.getRecentEntries(type: .coreMedia) allStats.append(contentsOf: CMPlayerParser.parseMusicConsoleLogs(musicLogs)) - //allStats.append(contentsOf: CMPlayerParser.parseCoreAudioConsoleLogs(coreAudioLogs)) - allStats.append(contentsOf: CMPlayerParser.parseCoreMediaConsoleLogs(coreMediaLogs)) + allStats.append(contentsOf: CMPlayerParser.parseCoreAudioConsoleLogs(coreAudioLogs)) + //allStats.append(contentsOf: CMPlayerParser.parseCoreMediaConsoleLogs(coreMediaLogs)) } allStats.sort(by: {$0.priority > $1.priority}) - print(allStats) - let defaultDevice = self.selectedOutputDevice ?? self.defaultOutputDevice - if let first = allStats.first, let supported = defaultDevice?.nominalSampleRates { - let sampleRate = Float64(first.sampleRate) - - if self.currentTrack == self.previousTrack, let prevSampleRate = currentSampleRate, prevSampleRate > sampleRate { - print("same track, prev sample rate is higher") - return - } - - if sampleRate == 48000 { - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - self.switchLatestSampleRate(recursion: true) - } - } - - // https://stackoverflow.com/a/65060134 - let nearest = supported.enumerated().min(by: { - abs($0.element - sampleRate) < abs($1.element - sampleRate) - }) - if let nearest = nearest { - let nearestSampleRate = nearest.element - if nearestSampleRate != previousSampleRate { - defaultDevice?.setNominalSampleRate(nearestSampleRate) - self.updateSampleRate(nearestSampleRate) - if let currentTrack = currentTrack { - self.trackAndSample[currentTrack] = nearestSampleRate - } - } - } + print("[getAllStats] \(allStats)") + } + catch { + print("[getAllStats, error] \(error)") + } + + return allStats + } + + func switchLatestSampleRate(recursion: Bool = false) { + let allStats = self.getAllStats() + let defaultDevice = self.selectedOutputDevice ?? self.defaultOutputDevice + + if let first = allStats.first, let supported = defaultDevice?.nominalSampleRates { + let sampleRate = Float64(first.sampleRate) + let bitDepth = Int32(first.bitDepth) + + if self.currentTrack == self.previousTrack/*, let prevSampleRate = currentSampleRate, prevSampleRate > sampleRate */ { + print("same track, prev sample rate is higher") + return } - else if !recursion { + + if sampleRate == 48000 { DispatchQueue.main.asyncAfter(deadline: .now() + 1) { self.switchLatestSampleRate(recursion: true) } } - else { -// print("cache \(self.trackAndSample)") - if self.currentTrack == self.previousTrack { - print("same track, ignore cache") - return - } - if let currentTrack = currentTrack, let cachedSampleRate = trackAndSample[currentTrack] { - print("using cached data") - if cachedSampleRate != previousSampleRate { - defaultDevice?.setNominalSampleRate(cachedSampleRate) - self.updateSampleRate(cachedSampleRate) + + let formats = self.getFormats(bestStat: first, device: defaultDevice!)! + + // https://stackoverflow.com/a/65060134 + let nearest = supported.min(by: { + abs($0 - sampleRate) < abs($1 - sampleRate) + }) + + let nearestBitDepth = formats.min(by: { + abs(Int32($0.mBitsPerChannel) - bitDepth) < abs(Int32($1.mBitsPerChannel) - bitDepth) + }) + + let nearestFormat = formats.filter({ + $0.mSampleRate == nearest && $0.mBitsPerChannel == nearestBitDepth?.mBitsPerChannel + }) + + print("NEAREST FORMAT \(nearestFormat)") + + if let suitableFormat = nearestFormat.first { + //if suitableFormat.mSampleRate != previousSampleRate { + self.setFormats(device: defaultDevice, format: suitableFormat) + self.updateSampleRate(suitableFormat.mSampleRate) + if let currentTrack = currentTrack { + self.trackAndSample[currentTrack] = suitableFormat.mSampleRate } - } + //} } + +// if let nearest = nearest { +// let nearestSampleRate = nearest.element +// if nearestSampleRate != previousSampleRate { +// defaultDevice?.setNominalSampleRate(nearestSampleRate) +// self.updateSampleRate(nearestSampleRate) +// if let currentTrack = currentTrack { +// self.trackAndSample[currentTrack] = nearestSampleRate +// } +// } +// } } - catch { - print(error) + else if !recursion { + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + self.switchLatestSampleRate(recursion: true) + } } + else { +// print("cache \(self.trackAndSample)") + if self.currentTrack == self.previousTrack { + print("same track, ignore cache") + return + } +// if let currentTrack = currentTrack, let cachedSampleRate = trackAndSample[currentTrack] { +// print("using cached data") +// if cachedSampleRate != previousSampleRate { +// defaultDevice?.setNominalSampleRate(cachedSampleRate) +// self.updateSampleRate(cachedSampleRate) +// } +// } + } + + } + + func getFormats(bestStat: CMPlayerStats, device: AudioDevice) -> [AudioStreamBasicDescription]? { + // new sample rate + bit depth detection route + let streams = device.streams(scope: .output) + let availableFormats = streams?.first?.availablePhysicalFormats?.compactMap({$0.mFormat}) + return availableFormats + } + + func setFormats(device: AudioDevice?, format: AudioStreamBasicDescription?) { + guard let device, let format else { return } + let streams = device.streams(scope: .output) + streams?.first?.physicalFormat = format } func updateSampleRate(_ sampleRate: Float64) { From af6caff39fdb8b1c4ef0ce0258de86304f5ee4fb Mon Sep 17 00:00:00 2001 From: Vincent Neo <23420208+vincentneo@users.noreply.github.com> Date: Tue, 28 Mar 2023 17:25:44 +0800 Subject: [PATCH 20/32] build number --- Quality.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Quality.xcodeproj/project.pbxproj b/Quality.xcodeproj/project.pbxproj index 13280b4..d4296a9 100644 --- a/Quality.xcodeproj/project.pbxproj +++ b/Quality.xcodeproj/project.pbxproj @@ -342,7 +342,7 @@ CODE_SIGN_ENTITLEMENTS = Quality/Quality.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 12; + CURRENT_PROJECT_VERSION = 13; DEVELOPMENT_ASSET_PATHS = "\"Quality/Preview Content\""; DEVELOPMENT_TEAM = 3X69W4AQD6; ENABLE_HARDENED_RUNTIME = YES; @@ -376,7 +376,7 @@ CODE_SIGN_ENTITLEMENTS = Quality/Quality.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 12; + CURRENT_PROJECT_VERSION = 13; DEVELOPMENT_ASSET_PATHS = "\"Quality/Preview Content\""; DEVELOPMENT_TEAM = 3X69W4AQD6; ENABLE_HARDENED_RUNTIME = YES; From a95d968bbe20340edbe2ff1c8f012cf54a8074b9 Mon Sep 17 00:00:00 2001 From: Vincent Neo <23420208+vincentneo@users.noreply.github.com> Date: Tue, 28 Mar 2023 17:37:35 +0800 Subject: [PATCH 21/32] remove applescript based detection --- Quality/AppDelegate.swift | 10 ---------- Quality/Defaults.swift | 15 ++------------- Quality/OutputDevices.swift | 22 ++++++---------------- 3 files changed, 8 insertions(+), 39 deletions(-) diff --git a/Quality/AppDelegate.swift b/Quality/AppDelegate.swift index 3cd1f01..64ee547 100644 --- a/Quality/AppDelegate.swift +++ b/Quality/AppDelegate.swift @@ -75,10 +75,6 @@ class AppDelegate: NSObject, NSApplicationDelegate { let showSampleRateItem = NSMenuItem(title: defaults.statusBarItemTitle, action: #selector(toggleSampleRate(item:)), keyEquivalent: "") menu.addItem(showSampleRateItem) - let useAppleScriptItem = NSMenuItem(title: "Detect Local Track Rates", action: #selector(toggleAppleScript(item:)), keyEquivalent: "") - menu.addItem(useAppleScriptItem) - useAppleScriptItem.state = Defaults.shared.userPreferAppleScript ? .on : .off - let selectedDeviceItem = NSMenuItem(title: "Selected Device", action: nil, keyEquivalent: "") self.devicesMenu = NSMenu() selectedDeviceItem.submenu = self.devicesMenu @@ -168,10 +164,4 @@ class AppDelegate: NSObject, NSApplicationDelegate { item.title = defaults.statusBarItemTitle } - @objc func toggleAppleScript(item: NSMenuItem) { - defaults.userPreferAppleScript = !defaults.userPreferAppleScript - outputDevices.enableAppleScript = defaults.userPreferAppleScript - item.state = defaults.userPreferAppleScript ? .on : .off - } - } diff --git a/Quality/Defaults.swift b/Quality/Defaults.swift index ad5162d..9962349 100644 --- a/Quality/Defaults.swift +++ b/Quality/Defaults.swift @@ -10,13 +10,11 @@ import Foundation class Defaults { static let shared = Defaults() private let kUserPreferIconStatusBarItem = "com.vincent-neo.LosslessSwitcher-Key-UserPreferIconStatusBarItem" - private let kUserPreferAppleScript = "com.vincent-neo.LosslessSwitcher-Key-UserPreferAppleScript" private let kSelectedDeviceUID = "com.vincent-neo.LosslessSwitcher-Key-SelectedDeviceUID" private init() { UserDefaults.standard.register(defaults: [ - kUserPreferIconStatusBarItem : true, - kUserPreferAppleScript : true + kUserPreferIconStatusBarItem : true ]) } @@ -29,15 +27,6 @@ class Defaults { } } - var userPreferAppleScript: Bool { - get { - return UserDefaults.standard.bool(forKey: kUserPreferAppleScript) - } - set { - UserDefaults.standard.set(newValue, forKey: kUserPreferAppleScript) - } - } - var selectedDeviceUID: String? { get { return UserDefaults.standard.string(forKey: kSelectedDeviceUID) @@ -46,7 +35,7 @@ class Defaults { UserDefaults.standard.set(newValue, forKey: kSelectedDeviceUID) } } - + var statusBarItemTitle: String { let title = self.userPreferIconStatusBarItem ? "Show Sample Rate" : "Show Icon" return title diff --git a/Quality/OutputDevices.swift b/Quality/OutputDevices.swift index 4cac261..a363ed0 100644 --- a/Quality/OutputDevices.swift +++ b/Quality/OutputDevices.swift @@ -16,8 +16,6 @@ class OutputDevices: ObservableObject { @Published var outputDevices = [AudioDevice]() @Published var currentSampleRate: Float64? - var enableAppleScript = Defaults.shared.userPreferAppleScript - private let coreAudio = SimplyCoreAudio() private var changesCancellable: AnyCancellable? @@ -147,20 +145,12 @@ class OutputDevices: ObservableObject { var allStats = [CMPlayerStats]() do { - if enableAppleScript { - if let appleScriptRate = getSampleRateFromAppleScript() { - print("AppleScript ran") - allStats.append(CMPlayerStats(sampleRate: appleScriptRate, bitDepth: 0, date: .init(), priority: 100)) - } - } - else { - let musicLogs = try Console.getRecentEntries(type: .music) - let coreAudioLogs = try Console.getRecentEntries(type: .coreAudio) - //let coreMediaLogs = try Console.getRecentEntries(type: .coreMedia) - allStats.append(contentsOf: CMPlayerParser.parseMusicConsoleLogs(musicLogs)) - allStats.append(contentsOf: CMPlayerParser.parseCoreAudioConsoleLogs(coreAudioLogs)) - //allStats.append(contentsOf: CMPlayerParser.parseCoreMediaConsoleLogs(coreMediaLogs)) - } + let musicLogs = try Console.getRecentEntries(type: .music) + let coreAudioLogs = try Console.getRecentEntries(type: .coreAudio) + let coreMediaLogs = try Console.getRecentEntries(type: .coreMedia) + allStats.append(contentsOf: CMPlayerParser.parseMusicConsoleLogs(musicLogs)) + //allStats.append(contentsOf: CMPlayerParser.parseCoreAudioConsoleLogs(coreAudioLogs)) + allStats.append(contentsOf: CMPlayerParser.parseCoreMediaConsoleLogs(coreMediaLogs)) allStats.sort(by: {$0.priority > $1.priority}) print("[getAllStats] \(allStats)") From 783b5f1773c407fa31e91a446890188bd91c8492 Mon Sep 17 00:00:00 2001 From: Vincent Neo <23420208+vincentneo@users.noreply.github.com> Date: Tue, 28 Mar 2023 17:43:29 +0800 Subject: [PATCH 22/32] additional AppleScript removal refer to #74 --- Quality/OutputDevices.swift | 30 ------------------------------ 1 file changed, 30 deletions(-) diff --git a/Quality/OutputDevices.swift b/Quality/OutputDevices.swift index a363ed0..42abe8e 100644 --- a/Quality/OutputDevices.swift +++ b/Quality/OutputDevices.swift @@ -111,36 +111,6 @@ class OutputDevices: ObservableObject { return nil } - let appleScriptQueue = DispatchQueue(label: "AppleScriptQueue") - - func getSampleRateFromAppleScript(_ completion: @escaping (Double?) -> ()) { - let scriptContents = "tell application \"Music\" to get sample rate of current track" - var error: NSDictionary? - - self.appleScriptQueue.async { - if let script = NSAppleScript(source: scriptContents) { - let output = script.executeAndReturnError(&error).stringValue - var dOutput: Double? - - defer { - completion(dOutput) - } - - if let error = error { - print("[APPLESCRIPT] - \(error)") - } - guard let output = output else { return } - - if output == "missing value" { - return - } - else { - dOutput = Double(output) - } - } - } - } - func getAllStats() -> [CMPlayerStats] { var allStats = [CMPlayerStats]() From 802f027aee5f88db20820d5fa124931d12711367 Mon Sep 17 00:00:00 2001 From: Vincent Neo <23420208+vincentneo@users.noreply.github.com> Date: Tue, 28 Mar 2023 18:02:40 +0800 Subject: [PATCH 23/32] bit depth detection now optional --- Quality.xcodeproj/project.pbxproj | 4 ++-- Quality/AppDelegate.swift | 11 +++++++++++ Quality/Defaults.swift | 16 ++++++++++++++-- Quality/OutputDevices.swift | 22 ++++++++++++++++++---- 4 files changed, 45 insertions(+), 8 deletions(-) diff --git a/Quality.xcodeproj/project.pbxproj b/Quality.xcodeproj/project.pbxproj index d4296a9..caa64a4 100644 --- a/Quality.xcodeproj/project.pbxproj +++ b/Quality.xcodeproj/project.pbxproj @@ -342,7 +342,7 @@ CODE_SIGN_ENTITLEMENTS = Quality/Quality.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 13; + CURRENT_PROJECT_VERSION = 14; DEVELOPMENT_ASSET_PATHS = "\"Quality/Preview Content\""; DEVELOPMENT_TEAM = 3X69W4AQD6; ENABLE_HARDENED_RUNTIME = YES; @@ -376,7 +376,7 @@ CODE_SIGN_ENTITLEMENTS = Quality/Quality.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 13; + CURRENT_PROJECT_VERSION = 14; DEVELOPMENT_ASSET_PATHS = "\"Quality/Preview Content\""; DEVELOPMENT_TEAM = 3X69W4AQD6; ENABLE_HARDENED_RUNTIME = YES; diff --git a/Quality/AppDelegate.swift b/Quality/AppDelegate.swift index 64ee547..6be0a8b 100644 --- a/Quality/AppDelegate.swift +++ b/Quality/AppDelegate.swift @@ -75,6 +75,10 @@ class AppDelegate: NSObject, NSApplicationDelegate { let showSampleRateItem = NSMenuItem(title: defaults.statusBarItemTitle, action: #selector(toggleSampleRate(item:)), keyEquivalent: "") menu.addItem(showSampleRateItem) + let enableBitDepthItem = NSMenuItem(title: "Bit Depth Switching", action: #selector(toggleBitDepthDetection(item:)), keyEquivalent: "") + menu.addItem(enableBitDepthItem) + enableBitDepthItem.state = defaults.userPreferBitDepthDetection ? .on : .off + let selectedDeviceItem = NSMenuItem(title: "Selected Device", action: nil, keyEquivalent: "") self.devicesMenu = NSMenu() selectedDeviceItem.submenu = self.devicesMenu @@ -164,4 +168,11 @@ class AppDelegate: NSObject, NSApplicationDelegate { item.title = defaults.statusBarItemTitle } + @objc func toggleBitDepthDetection(item: NSMenuItem) { + Task { + await defaults.setPreferBitDepthDetection(newValue: !defaults.userPreferBitDepthDetection) + item.state = defaults.userPreferBitDepthDetection ? .on : .off + } + } + } diff --git a/Quality/Defaults.swift b/Quality/Defaults.swift index 9962349..fc0d5d4 100644 --- a/Quality/Defaults.swift +++ b/Quality/Defaults.swift @@ -7,15 +7,19 @@ import Foundation -class Defaults { +class Defaults: ObservableObject { static let shared = Defaults() private let kUserPreferIconStatusBarItem = "com.vincent-neo.LosslessSwitcher-Key-UserPreferIconStatusBarItem" private let kSelectedDeviceUID = "com.vincent-neo.LosslessSwitcher-Key-SelectedDeviceUID" + private let kUserPreferBitDepthDetection = "com.vincent-neo.LosslessSwitcher-Key-BitDepthDetection" private init() { UserDefaults.standard.register(defaults: [ - kUserPreferIconStatusBarItem : true + kUserPreferIconStatusBarItem : true, + kUserPreferBitDepthDetection : false ]) + + self.userPreferBitDepthDetection = UserDefaults.standard.bool(forKey: kUserPreferBitDepthDetection) } var userPreferIconStatusBarItem: Bool { @@ -35,6 +39,14 @@ class Defaults { UserDefaults.standard.set(newValue, forKey: kSelectedDeviceUID) } } + + @Published var userPreferBitDepthDetection: Bool + + + @MainActor func setPreferBitDepthDetection(newValue: Bool) { + UserDefaults.standard.set(newValue, forKey: kUserPreferBitDepthDetection) + self.userPreferBitDepthDetection = newValue + } var statusBarItemTitle: String { let title = self.userPreferIconStatusBarItem ? "Show Sample Rate" : "Show Icon" diff --git a/Quality/OutputDevices.swift b/Quality/OutputDevices.swift index 42abe8e..98dd2c7 100644 --- a/Quality/OutputDevices.swift +++ b/Quality/OutputDevices.swift @@ -16,6 +16,9 @@ class OutputDevices: ObservableObject { @Published var outputDevices = [AudioDevice]() @Published var currentSampleRate: Float64? + private var enableBitDepthDetection = Defaults.shared.userPreferBitDepthDetection + private var enableBitDepthDetectionCancellable: AnyCancellable? + private let coreAudio = SimplyCoreAudio() private var changesCancellable: AnyCancellable? @@ -53,12 +56,18 @@ class OutputDevices: ObservableObject { self.getDeviceSampleRate() }) + enableBitDepthDetectionCancellable = Defaults.shared.$userPreferBitDepthDetection.sink(receiveValue: { newValue in + self.enableBitDepthDetection = newValue + }) + + } deinit { changesCancellable?.cancel() defaultChangesCancellable?.cancel() timerCancellable?.cancel() + enableBitDepthDetectionCancellable?.cancel() //timer.upstream.connect().cancel() } @@ -118,10 +127,15 @@ class OutputDevices: ObservableObject { let musicLogs = try Console.getRecentEntries(type: .music) let coreAudioLogs = try Console.getRecentEntries(type: .coreAudio) let coreMediaLogs = try Console.getRecentEntries(type: .coreMedia) - allStats.append(contentsOf: CMPlayerParser.parseMusicConsoleLogs(musicLogs)) - //allStats.append(contentsOf: CMPlayerParser.parseCoreAudioConsoleLogs(coreAudioLogs)) - allStats.append(contentsOf: CMPlayerParser.parseCoreMediaConsoleLogs(coreMediaLogs)) + allStats.append(contentsOf: CMPlayerParser.parseMusicConsoleLogs(musicLogs)) + if enableBitDepthDetection { + allStats.append(contentsOf: CMPlayerParser.parseCoreAudioConsoleLogs(coreAudioLogs)) + } + else { + allStats.append(contentsOf: CMPlayerParser.parseCoreMediaConsoleLogs(coreMediaLogs)) + } + allStats.sort(by: {$0.priority > $1.priority}) print("[getAllStats] \(allStats)") } @@ -140,7 +154,7 @@ class OutputDevices: ObservableObject { let sampleRate = Float64(first.sampleRate) let bitDepth = Int32(first.bitDepth) - if self.currentTrack == self.previousTrack/*, let prevSampleRate = currentSampleRate, prevSampleRate > sampleRate */ { + if self.currentTrack == self.previousTrack, let prevSampleRate = currentSampleRate, prevSampleRate > sampleRate { print("same track, prev sample rate is higher") return } From 6642445d81936c37776edb841e46a7dab6694a94 Mon Sep 17 00:00:00 2001 From: Vincent Neo <23420208+vincentneo@users.noreply.github.com> Date: Mon, 12 Jun 2023 00:57:15 +0800 Subject: [PATCH 24/32] fix #79 --- Quality/OutputDevices.swift | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/Quality/OutputDevices.swift b/Quality/OutputDevices.swift index 98dd2c7..3fb480b 100644 --- a/Quality/OutputDevices.swift +++ b/Quality/OutputDevices.swift @@ -183,13 +183,16 @@ class OutputDevices: ObservableObject { print("NEAREST FORMAT \(nearestFormat)") if let suitableFormat = nearestFormat.first { - //if suitableFormat.mSampleRate != previousSampleRate { + if enableBitDepthDetection { self.setFormats(device: defaultDevice, format: suitableFormat) - self.updateSampleRate(suitableFormat.mSampleRate) - if let currentTrack = currentTrack { - self.trackAndSample[currentTrack] = suitableFormat.mSampleRate - } - //} + } + else if suitableFormat.mSampleRate != previousSampleRate { // bit depth disabled + defaultDevice?.setNominalSampleRate(suitableFormat.mSampleRate) + } + self.updateSampleRate(suitableFormat.mSampleRate) + if let currentTrack = currentTrack { + self.trackAndSample[currentTrack] = suitableFormat.mSampleRate + } } // if let nearest = nearest { From c28893727a723e9fa3eaeef0c62afafdcdd90ec2 Mon Sep 17 00:00:00 2001 From: Vincent Neo <23420208+vincentneo@users.noreply.github.com> Date: Mon, 12 Jun 2023 00:57:34 +0800 Subject: [PATCH 25/32] increment build number --- Quality.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Quality.xcodeproj/project.pbxproj b/Quality.xcodeproj/project.pbxproj index caa64a4..4f10746 100644 --- a/Quality.xcodeproj/project.pbxproj +++ b/Quality.xcodeproj/project.pbxproj @@ -342,7 +342,7 @@ CODE_SIGN_ENTITLEMENTS = Quality/Quality.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 14; + CURRENT_PROJECT_VERSION = 15; DEVELOPMENT_ASSET_PATHS = "\"Quality/Preview Content\""; DEVELOPMENT_TEAM = 3X69W4AQD6; ENABLE_HARDENED_RUNTIME = YES; @@ -376,7 +376,7 @@ CODE_SIGN_ENTITLEMENTS = Quality/Quality.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 14; + CURRENT_PROJECT_VERSION = 15; DEVELOPMENT_ASSET_PATHS = "\"Quality/Preview Content\""; DEVELOPMENT_TEAM = 3X69W4AQD6; ENABLE_HARDENED_RUNTIME = YES; From 1584f53e1b7691313fdb08107d4ca9965466c076 Mon Sep 17 00:00:00 2001 From: Nandor Kracser Date: Fri, 25 Aug 2023 17:11:54 +0200 Subject: [PATCH 26/32] do less unnecessary work when bit-depth switching --- Quality/CMPlayerStuff.swift | 5 +++++ Quality/OutputDevices.swift | 4 +++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Quality/CMPlayerStuff.swift b/Quality/CMPlayerStuff.swift index 7c63eb7..6a7a1bd 100644 --- a/Quality/CMPlayerStuff.swift +++ b/Quality/CMPlayerStuff.swift @@ -26,6 +26,11 @@ class CMPlayerParser { var stats = [CMPlayerStats]() for entry in entries { + // ignore useless log messages for faster swithcing + if !entry.message.contains("audioCapabilities:") { + continue + } + let date = entry.date let rawMessage = entry.message diff --git a/Quality/OutputDevices.swift b/Quality/OutputDevices.swift index 3fb480b..1773cd1 100644 --- a/Quality/OutputDevices.swift +++ b/Quality/OutputDevices.swift @@ -238,7 +238,9 @@ class OutputDevices: ObservableObject { func setFormats(device: AudioDevice?, format: AudioStreamBasicDescription?) { guard let device, let format else { return } let streams = device.streams(scope: .output) - streams?.first?.physicalFormat = format + if streams?.first?.physicalFormat != format { + streams?.first?.physicalFormat = format + } } func updateSampleRate(_ sampleRate: Float64) { From bc678c2323503f28d0fa8fed50927478328f1615 Mon Sep 17 00:00:00 2001 From: Vincent Neo <23420208+vincentneo@users.noreply.github.com> Date: Thu, 31 Aug 2023 13:27:20 +0800 Subject: [PATCH 27/32] fix comment spelling --- Quality/CMPlayerStuff.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Quality/CMPlayerStuff.swift b/Quality/CMPlayerStuff.swift index 6a7a1bd..c34a829 100644 --- a/Quality/CMPlayerStuff.swift +++ b/Quality/CMPlayerStuff.swift @@ -26,7 +26,7 @@ class CMPlayerParser { var stats = [CMPlayerStats]() for entry in entries { - // ignore useless log messages for faster swithcing + // ignore useless log messages for faster switching if !entry.message.contains("audioCapabilities:") { continue } From 991f1e0a2fca03eb09b0a026d5b33596a074a64c Mon Sep 17 00:00:00 2001 From: Vincent Neo <23420208+vincentneo@users.noreply.github.com> Date: Thu, 31 Aug 2023 13:27:32 +0800 Subject: [PATCH 28/32] build number incremented to 16. --- Quality.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Quality.xcodeproj/project.pbxproj b/Quality.xcodeproj/project.pbxproj index 4f10746..c2e0858 100644 --- a/Quality.xcodeproj/project.pbxproj +++ b/Quality.xcodeproj/project.pbxproj @@ -342,7 +342,7 @@ CODE_SIGN_ENTITLEMENTS = Quality/Quality.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 15; + CURRENT_PROJECT_VERSION = 16; DEVELOPMENT_ASSET_PATHS = "\"Quality/Preview Content\""; DEVELOPMENT_TEAM = 3X69W4AQD6; ENABLE_HARDENED_RUNTIME = YES; @@ -376,7 +376,7 @@ CODE_SIGN_ENTITLEMENTS = Quality/Quality.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 15; + CURRENT_PROJECT_VERSION = 16; DEVELOPMENT_ASSET_PATHS = "\"Quality/Preview Content\""; DEVELOPMENT_TEAM = 3X69W4AQD6; ENABLE_HARDENED_RUNTIME = YES; From e8955f9b0e6c0a4676f896f26fd33f984e2592fb Mon Sep 17 00:00:00 2001 From: Vincent Neo <23420208+vincentneo@users.noreply.github.com> Date: Wed, 6 Dec 2023 23:10:47 +0800 Subject: [PATCH 29/32] add a way to get sample rate via AppleScript --- Quality.xcodeproj/project.pbxproj | 8 ++++++++ Quality/AppDelegate.swift | 2 +- Quality/Info.plist | 4 ++++ Quality/LosslessSwitcher.sdef | 10 ++++++++++ Quality/ScriptableApplicationCommand.swift | 22 ++++++++++++++++++++++ 5 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 Quality/LosslessSwitcher.sdef create mode 100644 Quality/ScriptableApplicationCommand.swift diff --git a/Quality.xcodeproj/project.pbxproj b/Quality.xcodeproj/project.pbxproj index c2e0858..a24675b 100644 --- a/Quality.xcodeproj/project.pbxproj +++ b/Quality.xcodeproj/project.pbxproj @@ -25,6 +25,8 @@ 1293436B28131591002E19A8 /* CurrentUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1293436A28131591002E19A8 /* CurrentUser.swift */; }; 12AFF5C12811AD40001CC6ED /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12AFF5C02811AD40001CC6ED /* AppDelegate.swift */; }; 12F1AA572868639A006C1AD8 /* DeviceMenuItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12F1AA562868639A006C1AD8 /* DeviceMenuItem.swift */; }; + BF0C90C82B20BFD6002F99C9 /* LosslessSwitcher.sdef in Resources */ = {isa = PBXBuildFile; fileRef = BF0C90C72B20BFD6002F99C9 /* LosslessSwitcher.sdef */; }; + BF0C90CA2B20C163002F99C9 /* ScriptableApplicationCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0C90C92B20C163002F99C9 /* ScriptableApplicationCommand.swift */; }; BF7E0D09296336DA009FFEEC /* AudioStreamBasicDescription+Equatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF7E0D08296336DA009FFEEC /* AudioStreamBasicDescription+Equatable.swift */; }; /* End PBXBuildFile section */ @@ -47,6 +49,8 @@ 1293436A28131591002E19A8 /* CurrentUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentUser.swift; sourceTree = ""; }; 12AFF5C02811AD40001CC6ED /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 12F1AA562868639A006C1AD8 /* DeviceMenuItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceMenuItem.swift; sourceTree = ""; }; + BF0C90C72B20BFD6002F99C9 /* LosslessSwitcher.sdef */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = LosslessSwitcher.sdef; sourceTree = ""; }; + BF0C90C92B20C163002F99C9 /* ScriptableApplicationCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScriptableApplicationCommand.swift; sourceTree = ""; }; BF7E0D08296336DA009FFEEC /* AudioStreamBasicDescription+Equatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AudioStreamBasicDescription+Equatable.swift"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -110,6 +114,8 @@ 127C972C281FCF000087313B /* AppVersion.swift */, 12F1AA562868639A006C1AD8 /* DeviceMenuItem.swift */, BF7E0D08296336DA009FFEEC /* AudioStreamBasicDescription+Equatable.swift */, + BF0C90C92B20C163002F99C9 /* ScriptableApplicationCommand.swift */, + BF0C90C72B20BFD6002F99C9 /* LosslessSwitcher.sdef */, ); path = Quality; sourceTree = ""; @@ -191,6 +197,7 @@ buildActionMask = 2147483647; files = ( 1272AA9F280DBB4B00FD72BA /* Preview Assets.xcassets in Resources */, + BF0C90C82B20BFD6002F99C9 /* LosslessSwitcher.sdef in Resources */, 1272AA9C280DBB4B00FD72BA /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -203,6 +210,7 @@ buildActionMask = 2147483647; files = ( 12AFF5C12811AD40001CC6ED /* AppDelegate.swift in Sources */, + BF0C90CA2B20C163002F99C9 /* ScriptableApplicationCommand.swift in Sources */, 1254A79C2813FB9400241107 /* Defaults.swift in Sources */, BF7E0D09296336DA009FFEEC /* AudioStreamBasicDescription+Equatable.swift in Sources */, 1234F50E281E8F07007EC9F5 /* MediaTrack.swift in Sources */, diff --git a/Quality/AppDelegate.swift b/Quality/AppDelegate.swift index 6be0a8b..03d157c 100644 --- a/Quality/AppDelegate.swift +++ b/Quality/AppDelegate.swift @@ -15,7 +15,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { // https://stackoverflow.com/a/66160164 static private(set) var instance: AppDelegate! = nil - private var outputDevices: OutputDevices! + var outputDevices: OutputDevices! private let defaults = Defaults.shared private var mrController: MediaRemoteController! private var devicesMenu: NSMenu! diff --git a/Quality/Info.plist b/Quality/Info.plist index deaca6f..12822ad 100644 --- a/Quality/Info.plist +++ b/Quality/Info.plist @@ -6,5 +6,9 @@ NSAppleEventsUsageDescription This permission is required for local file sample rate detection. + NSAppleScriptEnabled + + OSAScriptingDefinition + LosslessSwitcher.sdef diff --git a/Quality/LosslessSwitcher.sdef b/Quality/LosslessSwitcher.sdef new file mode 100644 index 0000000..766ce16 --- /dev/null +++ b/Quality/LosslessSwitcher.sdef @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/Quality/ScriptableApplicationCommand.swift b/Quality/ScriptableApplicationCommand.swift new file mode 100644 index 0000000..a0d8d00 --- /dev/null +++ b/Quality/ScriptableApplicationCommand.swift @@ -0,0 +1,22 @@ +// +// ScriptableApplicationCommand.swift +// LosslessSwitcher +// +// Created by Vincent Neo on 6/12/23. +// + +import Cocoa + +class ScriptableApplicationCommand: NSScriptCommand { + + override func performDefaultImplementation() -> Any? { + guard let delegate = AppDelegate.instance else { + return -1000 + } + let od = delegate.outputDevices + guard let sampleRate = od?.currentSampleRate else { + return -1 + } + return Int(sampleRate * 1000) + } +} From 84bf620404134d9cbb33f00d476265b300c03fef Mon Sep 17 00:00:00 2001 From: Vincent Neo <23420208+vincentneo@users.noreply.github.com> Date: Thu, 7 Dec 2023 23:37:50 +0800 Subject: [PATCH 30/32] build number increment --- Quality.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Quality.xcodeproj/project.pbxproj b/Quality.xcodeproj/project.pbxproj index a24675b..8e10fdf 100644 --- a/Quality.xcodeproj/project.pbxproj +++ b/Quality.xcodeproj/project.pbxproj @@ -350,7 +350,7 @@ CODE_SIGN_ENTITLEMENTS = Quality/Quality.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 16; + CURRENT_PROJECT_VERSION = 17; DEVELOPMENT_ASSET_PATHS = "\"Quality/Preview Content\""; DEVELOPMENT_TEAM = 3X69W4AQD6; ENABLE_HARDENED_RUNTIME = YES; @@ -384,7 +384,7 @@ CODE_SIGN_ENTITLEMENTS = Quality/Quality.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 16; + CURRENT_PROJECT_VERSION = 17; DEVELOPMENT_ASSET_PATHS = "\"Quality/Preview Content\""; DEVELOPMENT_TEAM = 3X69W4AQD6; ENABLE_HARDENED_RUNTIME = YES; From 1d8a8919d23b65f27d709dec6d96c192746585d6 Mon Sep 17 00:00:00 2001 From: Vincent Neo <23420208+vincentneo@users.noreply.github.com> Date: Thu, 7 Dec 2023 23:38:37 +0800 Subject: [PATCH 31/32] Option to run script when sample rate change --- Quality/AppDelegate.swift | 37 +++++++++++++++++++++++++++++++++++++ Quality/Defaults.swift | 10 ++++++++++ Quality/OutputDevices.swift | 19 +++++++++++++++++++ 3 files changed, 66 insertions(+) diff --git a/Quality/AppDelegate.swift b/Quality/AppDelegate.swift index 03d157c..7abf513 100644 --- a/Quality/AppDelegate.swift +++ b/Quality/AppDelegate.swift @@ -22,6 +22,8 @@ class AppDelegate: NSObject, NSApplicationDelegate { var statusItem: NSStatusItem? var cancellable: AnyCancellable? + + var currentScriptSelectionMenuItem: NSMenuItem? private var _statusItemTitle = "Loading..." var statusItemTitle: String { @@ -62,6 +64,8 @@ class AppDelegate: NSObject, NSApplicationDelegate { checkPermissions() let menu = NSMenu() + + menu.delegate = self let sampleRateView = ContentView().environmentObject(outputDevices) let view = NSHostingView(rootView: sampleRateView) @@ -96,6 +100,17 @@ class AppDelegate: NSObject, NSApplicationDelegate { aboutItem.submenu?.addItem(buildItem) menu.addItem(aboutItem) + let scriptMenu = NSMenuItem(title: "Scripting", action: nil, keyEquivalent: "") + let selectScript = NSMenuItem(title: "Select Script...", action: #selector(selectScript(_:)), keyEquivalent: "") + let resetScript = NSMenuItem(title: "Clear selection", action: #selector(resetScript(_:)), keyEquivalent: "") + let currentScriptSelectionMenuItem = NSMenuItem(title: "No selection", action: nil, keyEquivalent: "") + self.currentScriptSelectionMenuItem = currentScriptSelectionMenuItem + scriptMenu.submenu = NSMenu() + scriptMenu.submenu?.addItem(selectScript) + scriptMenu.submenu?.addItem(resetScript) + scriptMenu.submenu?.addItem(currentScriptSelectionMenuItem) + menu.addItem(scriptMenu) + let quitItem = NSMenuItem(title: "Quit", action: #selector(NSApp.terminate(_:)), keyEquivalent: "") menu.addItem(quitItem) @@ -175,4 +190,26 @@ class AppDelegate: NSObject, NSApplicationDelegate { } } + @objc func selectScript(_ item: NSMenuItem) { + let panel = NSOpenPanel() + panel.canChooseFiles = true + panel.canChooseDirectories = false + panel.allowsMultipleSelection = false + panel.message = "Select a script that should be invoked when sample rate changes." + + panel.begin { response in + Defaults.shared.shellScriptPath = panel.url?.path + } + } + + @objc func resetScript(_ item: NSMenuItem) { + Defaults.shared.shellScriptPath = nil + } + +} + +extension AppDelegate: NSMenuDelegate { + func menuWillOpen(_ menu: NSMenu) { + currentScriptSelectionMenuItem?.title = Defaults.shared.shellScriptPath ?? "No selection" + } } diff --git a/Quality/Defaults.swift b/Quality/Defaults.swift index fc0d5d4..a31ad76 100644 --- a/Quality/Defaults.swift +++ b/Quality/Defaults.swift @@ -12,6 +12,7 @@ class Defaults: ObservableObject { private let kUserPreferIconStatusBarItem = "com.vincent-neo.LosslessSwitcher-Key-UserPreferIconStatusBarItem" private let kSelectedDeviceUID = "com.vincent-neo.LosslessSwitcher-Key-SelectedDeviceUID" private let kUserPreferBitDepthDetection = "com.vincent-neo.LosslessSwitcher-Key-BitDepthDetection" + private let kShellScriptPath = "KeyShellScriptPath" private init() { UserDefaults.standard.register(defaults: [ @@ -40,6 +41,15 @@ class Defaults: ObservableObject { } } + var shellScriptPath: String? { + get { + return UserDefaults.standard.string(forKey: kShellScriptPath) + } + set { + UserDefaults.standard.setValue(newValue, forKey: kShellScriptPath) + } + } + @Published var userPreferBitDepthDetection: Bool diff --git a/Quality/OutputDevices.swift b/Quality/OutputDevices.swift index 1773cd1..78357ff 100644 --- a/Quality/OutputDevices.swift +++ b/Quality/OutputDevices.swift @@ -252,5 +252,24 @@ class OutputDevices: ObservableObject { let delegate = AppDelegate.instance delegate?.statusItemTitle = String(format: "%.1f kHz", readableSampleRate) } + self.runUserScript(sampleRate) + } + + func runUserScript(_ sampleRate: Float64) { + guard let scriptPath = Defaults.shared.shellScriptPath else { return } + let argumentSampleRate = String(Int(sampleRate)) + Task.detached { + let scriptURL = URL(fileURLWithPath: scriptPath) + do { + let task = try NSUserUnixTask(url: scriptURL) + let arguments = [ + argumentSampleRate + ] + try await task.execute(withArguments: arguments) + } + catch { + print("TASK ERR \(error)") + } + } } } From 9937c33c05d4d574fd03fac1434fee85386f626e Mon Sep 17 00:00:00 2001 From: Vincent Neo <23420208+vincentneo@users.noreply.github.com> Date: Thu, 7 Dec 2023 23:42:27 +0800 Subject: [PATCH 32/32] Increment build number to 18 --- Quality.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Quality.xcodeproj/project.pbxproj b/Quality.xcodeproj/project.pbxproj index 8e10fdf..cdb18ad 100644 --- a/Quality.xcodeproj/project.pbxproj +++ b/Quality.xcodeproj/project.pbxproj @@ -350,7 +350,7 @@ CODE_SIGN_ENTITLEMENTS = Quality/Quality.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 17; + CURRENT_PROJECT_VERSION = 18; DEVELOPMENT_ASSET_PATHS = "\"Quality/Preview Content\""; DEVELOPMENT_TEAM = 3X69W4AQD6; ENABLE_HARDENED_RUNTIME = YES; @@ -384,7 +384,7 @@ CODE_SIGN_ENTITLEMENTS = Quality/Quality.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 17; + CURRENT_PROJECT_VERSION = 18; DEVELOPMENT_ASSET_PATHS = "\"Quality/Preview Content\""; DEVELOPMENT_TEAM = 3X69W4AQD6; ENABLE_HARDENED_RUNTIME = YES;