Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 18 additions & 14 deletions Quality.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
objects = {

/* Begin PBXBuildFile section */
0177D9D329F0986700703F3C /* NetworkServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0177D9D229F0986700703F3C /* NetworkServer.swift */; };
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 */; };
Expand All @@ -25,10 +26,12 @@
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 */; };
BF2717B42B8A466A00D3E7C9 /* Network.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF2717B32B8A466A00D3E7C9 /* Network.framework */; };
BF7E0D09296336DA009FFEEC /* AudioStreamBasicDescription+Equatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF7E0D08296336DA009FFEEC /* AudioStreamBasicDescription+Equatable.swift */; };
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
0177D9D229F0986700703F3C /* NetworkServer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkServer.swift; sourceTree = "<group>"; };
1221F3FA280F1EEF003E8B77 /* OutputDevices.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutputDevices.swift; sourceTree = "<group>"; };
1234F509281E83D1007EC9F5 /* MediaRemote.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MediaRemote.framework; path = ../../../../../System/Library/PrivateFrameworks/MediaRemote.framework; sourceTree = "<group>"; };
1234F50D281E8F07007EC9F5 /* MediaTrack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaTrack.swift; sourceTree = "<group>"; };
Expand All @@ -47,6 +50,7 @@
1293436A28131591002E19A8 /* CurrentUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentUser.swift; sourceTree = "<group>"; };
12AFF5C02811AD40001CC6ED /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
12F1AA562868639A006C1AD8 /* DeviceMenuItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceMenuItem.swift; sourceTree = "<group>"; };
BF2717B32B8A466A00D3E7C9 /* Network.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Network.framework; path = System/Library/Frameworks/Network.framework; sourceTree = SDKROOT; };
BF7E0D08296336DA009FFEEC /* AudioStreamBasicDescription+Equatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AudioStreamBasicDescription+Equatable.swift"; sourceTree = "<group>"; };
/* End PBXFileReference section */

Expand All @@ -59,6 +63,7 @@
1272AAB1280DC71B00FD72BA /* Sweep in Frameworks */,
1234F50A281E83D1007EC9F5 /* MediaRemote.framework in Frameworks */,
1234F508281E8372007EC9F5 /* PrivateMediaRemote in Frameworks */,
BF2717B42B8A466A00D3E7C9 /* Network.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand All @@ -68,6 +73,7 @@
1234F4FD281E80A2007EC9F5 /* Frameworks */ = {
isa = PBXGroup;
children = (
BF2717B32B8A466A00D3E7C9 /* Network.framework */,
1234F509281E83D1007EC9F5 /* MediaRemote.framework */,
);
name = Frameworks;
Expand All @@ -94,6 +100,7 @@
isa = PBXGroup;
children = (
1254A79D2814024300241107 /* Info.plist */,
0177D9D229F0986700703F3C /* NetworkServer.swift */,
12AFF5C02811AD40001CC6ED /* AppDelegate.swift */,
1272AA97280DBB4900FD72BA /* QualityApp.swift */,
1272AA99280DBB4900FD72BA /* ContentView.swift */,
Expand Down Expand Up @@ -213,6 +220,7 @@
1234F510281E9520007EC9F5 /* MediaRemoteController.swift in Sources */,
1272AA9A280DBB4900FD72BA /* ContentView.swift in Sources */,
1293436B28131591002E19A8 /* CurrentUser.swift in Sources */,
0177D9D329F0986700703F3C /* NetworkServer.swift in Sources */,
1272AA98280DBB4900FD72BA /* QualityApp.swift in Sources */,
127C972D281FCF000087313B /* AppVersion.swift in Sources */,
);
Expand Down Expand Up @@ -342,11 +350,12 @@
CODE_SIGN_ENTITLEMENTS = Quality/Quality.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 14;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"Quality/Preview Content\"";
DEVELOPMENT_TEAM = 3X69W4AQD6;
DEVELOPMENT_TEAM = "";
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
FRAMEWORK_SEARCH_PATHS = "$(SYSTEM_LIBRARY_DIR)/PrivateFrameworks";
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = Quality/Info.plist;
INFOPLIST_KEY_NSAppleEventsUsageDescription = "This permission is required for local file sample rate detection.";
Expand All @@ -356,15 +365,12 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 11.4;
MARKETING_VERSION = 1.1;
MARKETING_VERSION = 1.2;
PRODUCT_BUNDLE_IDENTIFIER = "com.vincent-neo.LosslessSwitcher";
PRODUCT_NAME = LosslessSwitcher;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
SYSTEM_FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(SYSTEM_LIBRARY_DIR)/PrivateFrameworks",
);
SYSTEM_FRAMEWORK_SEARCH_PATHS = "$(inherited)";
};
name = Debug;
};
Expand All @@ -376,11 +382,12 @@
CODE_SIGN_ENTITLEMENTS = Quality/Quality.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 14;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"Quality/Preview Content\"";
DEVELOPMENT_TEAM = 3X69W4AQD6;
DEVELOPMENT_TEAM = "";
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
FRAMEWORK_SEARCH_PATHS = "$(SYSTEM_LIBRARY_DIR)/PrivateFrameworks";
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = Quality/Info.plist;
INFOPLIST_KEY_NSAppleEventsUsageDescription = "This permission is required for local file sample rate detection.";
Expand All @@ -390,15 +397,12 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 11.4;
MARKETING_VERSION = 1.1;
MARKETING_VERSION = 1.2;
PRODUCT_BUNDLE_IDENTIFIER = "com.vincent-neo.LosslessSwitcher";
PRODUCT_NAME = LosslessSwitcher;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
SYSTEM_FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(SYSTEM_LIBRARY_DIR)/PrivateFrameworks",
);
SYSTEM_FRAMEWORK_SEARCH_PATHS = "$(inherited)";
};
name = Release;
};
Expand Down
115 changes: 111 additions & 4 deletions Quality/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,14 @@ class AppDelegate: NSObject, NSApplicationDelegate {
private var outputDevices: OutputDevices!
private let defaults = Defaults.shared
private var mrController: MediaRemoteController!
private var networkServer: NetworkServer!
private var devicesMenu: NSMenu!
private var rateMenu: NSMenu!
private var bitDepthMenu: NSMenu!

var statusItem: NSStatusItem?
var cancellable: AnyCancellable?
private var refreshedSampleRatesAndBitDepths: AnyCancellable?

private var _statusItemTitle = "Loading..."
var statusItemTitle: String {
Expand Down Expand Up @@ -58,6 +62,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
AppDelegate.instance = self
outputDevices = OutputDevices()
mrController = MediaRemoteController(outputDevices: outputDevices)
networkServer = NetworkServer(outputDevices)

checkPermissions()

Expand All @@ -72,19 +77,38 @@ class AppDelegate: NSObject, NSApplicationDelegate {

menu.addItem(NSMenuItem.separator())

let showSampleRateItem = NSMenuItem(title: defaults.statusBarItemTitle, action: #selector(toggleSampleRate(item:)), keyEquivalent: "")
menu.addItem(showSampleRateItem)
let setSampleRateItem = NSMenuItem(title: "Set Current to Detected", action: #selector(setCurrentToDetected), keyEquivalent: "")
menu.addItem(setSampleRateItem)

let enableBitDepthItem = NSMenuItem(title: "Bit Depth Switching", action: #selector(toggleBitDepthDetection(item:)), keyEquivalent: "")
let enableAutoSwitchItem = NSMenuItem(title: "Auto Switching", action: #selector(toggleAutoSwitching(item:)), keyEquivalent: "")
menu.addItem(enableAutoSwitchItem)
enableAutoSwitchItem.state = defaults.userPreferAutoSwitch ? .on : .off

let enableBitDepthItem = NSMenuItem(title: "Bit Depth Detection", action: #selector(toggleBitDepthDetection(item:)), keyEquivalent: "")
menu.addItem(enableBitDepthItem)
enableBitDepthItem.state = defaults.userPreferBitDepthDetection ? .on : .off

let setRateItem = NSMenuItem(title: "Set Rate", action: nil, keyEquivalent: "")
self.rateMenu = NSMenu()
setRateItem.submenu = self.rateMenu
menu.addItem(setRateItem)
self.handleRateMenu()

let setBitDepthItem = NSMenuItem(title: "Set Bit Depth", action: nil, keyEquivalent: "")
self.bitDepthMenu = NSMenu()
setBitDepthItem.submenu = self.bitDepthMenu
menu.addItem(setBitDepthItem)
self.handleBitDepthMenu()

let selectedDeviceItem = NSMenuItem(title: "Selected Device", action: nil, keyEquivalent: "")
self.devicesMenu = NSMenu()
selectedDeviceItem.submenu = self.devicesMenu
menu.addItem(selectedDeviceItem)
self.handleDevicesMenu()

let showSampleRateItem = NSMenuItem(title: defaults.statusBarItemTitle, action: #selector(toggleSampleRate(item:)), keyEquivalent: "")
menu.addItem(showSampleRateItem)

menu.addItem(NSMenuItem.separator())

let aboutItem = NSMenuItem(title: "About", action: nil, keyEquivalent: "")
Expand All @@ -107,7 +131,41 @@ class AppDelegate: NSObject, NSApplicationDelegate {
cancellable = NotificationCenter.default.publisher(for: .deviceListChanged).sink(receiveValue: { _ in
self.handleDevicesMenu()
})


refreshedSampleRatesAndBitDepths = NotificationCenter.default.publisher(for: .refreshedSampleRatesAndBitDepths).sink(receiveValue: { _ in
self.handleRateMenu()
self.handleBitDepthMenu()
})

networkServer.startListener()
}

func handleRateMenu() {
self.rateMenu.removeAllItems()
var idx = 0
for rate in outputDevices.sampleRatesForCurrentBitDepth {
//let rateTitle = "\(rate.mSampleRate / 1000) kHz - \(rate.mBitsPerChannel) bit"
let rateTitle = "\(rate.mSampleRate / 1000) kHz"
let rateItem = NSMenuItem(title: rateTitle, action: #selector(sampleRateSelected(_:)), keyEquivalent: "")
rateItem.tag = idx
rateItem.target = self
self.rateMenu.addItem(rateItem)
idx += 1
}
}

func handleBitDepthMenu() {
self.bitDepthMenu.removeAllItems()
var idx = 0
for format in outputDevices.bitDepthsForCurrentSampleRate {
//let formatTitle = "\(format.mBitsPerChannel) bit - \(format.mSampleRate / 1000) kHz"
let formatTitle = "\(format.mBitsPerChannel) bit"
let formatItem = NSMenuItem(title: formatTitle, action: #selector(formatSelected(_:)), keyEquivalent: "")
formatItem.tag = idx
formatItem.target = self
self.bitDepthMenu.addItem(formatItem)
idx += 1
}
}

func handleDevicesMenu() {
Expand Down Expand Up @@ -175,4 +233,53 @@ class AppDelegate: NSObject, NSApplicationDelegate {
}
}

@objc func toggleAutoSwitching(item: NSMenuItem) {
Task {
await defaults.setPreferAutoSwitch(newValue: !defaults.userPreferAutoSwitch)
item.state = defaults.userPreferAutoSwitch ? .on : .off
}
}

@objc func setCurrentToDetected() {
outputDevices.setCurrentToDetected()
}


@objc func sampleRateSelected(_ sender: NSMenuItem) {
let selectedIndex = sender.tag
let selectedRate = outputDevices.sampleRatesForCurrentBitDepth[selectedIndex]
outputDevices.manualSetFormat(selectedRate)
}

@objc func formatSelected(_ sender: NSMenuItem) {
let selectedIndex = sender.tag
let selectedFormat = outputDevices.bitDepthsForCurrentSampleRate[selectedIndex]
outputDevices.manualSetFormat(selectedFormat)
}

func updateAutoSwitchingMenuItemState() {
if let menu = statusItem?.menu,
let enableAutoSwitchItem = menu.item(withTitle: "Auto Switching") {
enableAutoSwitchItem.state = outputDevices.enableAutoSwitch ? .on : .off
}
}

func updateBitDepthDetectionMenuItemState() {
if let menu = statusItem?.menu,
let enableBitDepthDetectionItem = menu.item(withTitle: "Bit Depth Detection") {
enableBitDepthDetectionItem.state = outputDevices.enableBitDepthDetection ? .on : .off
}
}

func updateClients() {
if let networkServer = self.networkServer {
networkServer.updateClients()
}
}

deinit {
cancellable?.cancel()
refreshedSampleRatesAndBitDepths?.cancel()
}

}
27 changes: 22 additions & 5 deletions Quality/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,30 @@ import SimplyCoreAudio
struct ContentView: View {
@EnvironmentObject var outputDevices: OutputDevices

private var formattedCurrentSettings: String {
let currentSampleRate = outputDevices.currentSampleRate ?? 1
let currentBitDepth = outputDevices.currentBitDepth ?? 0
return "C: \(outputDevices.kHzString(currentSampleRate))kHz \(currentBitDepth)bit"
}

private var formattedDetectedSettings: String {
let detectedSampleRate = outputDevices.detectedSampleRate ?? 1
let detectedBitDepth = outputDevices.detectedBitDepth ?? 0
var formattedSettings = "D: \(outputDevices.kHzString(detectedSampleRate))kHz"
if outputDevices.enableBitDepthDetection {
formattedSettings += " \(detectedBitDepth)bit"
}
return formattedSettings
}

var body: some View {
VStack {
if let currentSampleRate = outputDevices.currentSampleRate {
let formattedSampleRate = String(format: "%.1f kHz", currentSampleRate)
Text(formattedSampleRate)
.font(.system(size: 23, weight: .semibold, design: .default))
}
Text(formattedCurrentSettings)
.font(.system(size: 23, weight: .semibold, design: .default))
.lineLimit(1)
Text(formattedDetectedSettings)
.font(.system(size: 23, weight: .semibold, design: .default))
.lineLimit(1)
if let device = outputDevices.selectedOutputDevice ?? outputDevices.defaultOutputDevice {
Text(device.name)
.font(.system(size: 14.5, weight: .regular, design: .default))
Expand Down
17 changes: 13 additions & 4 deletions Quality/Defaults.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,23 @@ import Foundation

class Defaults: ObservableObject {
static let shared = Defaults()
@Published var userPreferBitDepthDetection: Bool
@Published var userPreferAutoSwitch: Bool

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 kUserPreferAutoSwitch = "com.vincent-neo.LosslessSwitcher-Key-AutoSwitch"

private init() {
UserDefaults.standard.register(defaults: [
kUserPreferIconStatusBarItem : true,
kUserPreferBitDepthDetection : false
kUserPreferBitDepthDetection : false,
kUserPreferAutoSwitch : true
])

self.userPreferBitDepthDetection = UserDefaults.standard.bool(forKey: kUserPreferBitDepthDetection)
self.userPreferAutoSwitch = UserDefaults.standard.bool(forKey: kUserPreferAutoSwitch)
}

var userPreferIconStatusBarItem: Bool {
Expand All @@ -40,16 +46,19 @@ class Defaults: ObservableObject {
}
}

@Published var userPreferBitDepthDetection: Bool


@MainActor func setPreferBitDepthDetection(newValue: Bool) {
UserDefaults.standard.set(newValue, forKey: kUserPreferBitDepthDetection)
self.userPreferBitDepthDetection = newValue
}

@MainActor func setPreferAutoSwitch(newValue: Bool) {
UserDefaults.standard.set(newValue, forKey: kUserPreferAutoSwitch)
self.userPreferAutoSwitch = newValue
}

var statusBarItemTitle: String {
let title = self.userPreferIconStatusBarItem ? "Show Sample Rate" : "Show Icon"
return title
}

}
Loading