Skip to content

Commit

Permalink
support bit depth switching
Browse files Browse the repository at this point in the history
  • Loading branch information
vincentneo committed Jan 3, 2023
1 parent 22bc619 commit 1fd4087
Show file tree
Hide file tree
Showing 4 changed files with 158 additions and 61 deletions.
4 changes: 4 additions & 0 deletions Quality.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand All @@ -46,6 +47,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>"; };
BF7E0D08296336DA009FFEEC /* AudioStreamBasicDescription+Equatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AudioStreamBasicDescription+Equatable.swift"; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -107,6 +109,7 @@
1234F50F281E9520007EC9F5 /* MediaRemoteController.swift */,
127C972C281FCF000087313B /* AppVersion.swift */,
12F1AA562868639A006C1AD8 /* DeviceMenuItem.swift */,
BF7E0D08296336DA009FFEEC /* AudioStreamBasicDescription+Equatable.swift */,
);
path = Quality;
sourceTree = "<group>";
Expand Down Expand Up @@ -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 */,
Expand Down
14 changes: 14 additions & 0 deletions Quality/AudioStreamBasicDescription+Equatable.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
11 changes: 7 additions & 4 deletions Quality/CMPlayerStuff.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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]()

Expand All @@ -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)")
Expand Down
190 changes: 133 additions & 57 deletions Quality/OutputDevices.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import Combine
import Foundation
import SimplyCoreAudio
import CoreAudioTypes

class OutputDevices: ObservableObject {
@Published var selectedOutputDevice: AudioDevice? // auto if nil
Expand Down Expand Up @@ -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) {
Expand Down

0 comments on commit 1fd4087

Please sign in to comment.