diff --git a/Aural.xcodeproj/project.pbxproj b/Aural.xcodeproj/project.pbxproj index 5062aaa7b..bc2d2e40c 100644 --- a/Aural.xcodeproj/project.pbxproj +++ b/Aural.xcodeproj/project.pbxproj @@ -813,6 +813,7 @@ 3E7E9F4C2C66B06F0011DE8E /* WaveformViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E7E9F4B2C66B06F0011DE8E /* WaveformViewController.swift */; }; 3E7E9F4E2C66B1AF0011DE8E /* WaveformWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E7E9F4D2C66B1AF0011DE8E /* WaveformWindowController.swift */; }; 3E804D222C29F7840049AC27 /* GestureHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E804D212C29F7840049AC27 /* GestureHandler.swift */; }; + 3E8145C62C7A3850005BA9B9 /* AlbumReplayGain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E8145C52C7A3850005BA9B9 /* AlbumReplayGain.swift */; }; 3E836E332868964D009371D4 /* PlayQueueContainer.xib in Resources */ = {isa = PBXBuildFile; fileRef = 3E836E322868964D009371D4 /* PlayQueueContainer.xib */; }; 3E836E3B2868B9FD009371D4 /* UnifiedPlayerWindowController+EventHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E836E3A2868B9FD009371D4 /* UnifiedPlayerWindowController+EventHandling.swift */; }; 3E86B5072C234A420097746A /* buildingLibrary.gif in Resources */ = {isa = PBXBuildFile; fileRef = 3E86B5062C234A420097746A /* buildingLibrary.gif */; }; @@ -1928,6 +1929,7 @@ 3E7E9F4B2C66B06F0011DE8E /* WaveformViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaveformViewController.swift; sourceTree = ""; }; 3E7E9F4D2C66B1AF0011DE8E /* WaveformWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaveformWindowController.swift; sourceTree = ""; }; 3E804D212C29F7840049AC27 /* GestureHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GestureHandler.swift; sourceTree = ""; }; + 3E8145C52C7A3850005BA9B9 /* AlbumReplayGain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbumReplayGain.swift; sourceTree = ""; }; 3E836E322868964D009371D4 /* PlayQueueContainer.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = PlayQueueContainer.xib; sourceTree = ""; }; 3E836E3A2868B9FD009371D4 /* UnifiedPlayerWindowController+EventHandling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UnifiedPlayerWindowController+EventHandling.swift"; sourceTree = ""; }; 3E86B5062C234A420097746A /* buildingLibrary.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = buildingLibrary.gif; sourceTree = ""; }; @@ -3015,6 +3017,7 @@ 3E02175B2C23490E00865AC2 /* MetadataType.swift */, 3E02175C2C23490E00865AC2 /* PrimaryMetadata.swift */, 3E5701952C6EB16A007B8611 /* ReplayGain.swift */, + 3E8145C52C7A3850005BA9B9 /* AlbumReplayGain.swift */, 3E02175D2C23490E00865AC2 /* Track.swift */, ); path = Model; @@ -6076,6 +6079,7 @@ 3E02196D2C23490E00865AC2 /* PlaylistViewSelector.swift in Sources */, 3EAFB615267FF47200F0DC96 /* FilterBandSlider.swift in Sources */, 3EBF29292686947700D87021 /* FilterBandsTabButtonCell.swift in Sources */, + 3E8145C62C7A3850005BA9B9 /* AlbumReplayGain.swift in Sources */, 3E6C127025CEBE0600BF0D07 /* FilterPresetBandsViewDelegate.swift in Sources */, 3E6C12E625CEBE8100BF0D07 /* FilterBandViewController.swift in Sources */, 3E0219322C23490E00865AC2 /* AudioInfo.swift in Sources */, @@ -6708,6 +6712,7 @@ GCC_WARN_UNUSED_LABEL = YES; GCC_WARN_UNUSED_PARAMETER = YES; GCC_WARN_UNUSED_VARIABLE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = "// Copyright © 2024 Kartik Venugopal. All rights reserved.\n//\n// This software is licensed under the MIT software license.\n// See the file \"LICENSE\" in the project root directory for license terms.\n//"; MACOSX_DEPLOYMENT_TARGET = 10.12; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; @@ -6765,6 +6770,7 @@ GCC_WARN_UNUSED_LABEL = YES; GCC_WARN_UNUSED_PARAMETER = YES; GCC_WARN_UNUSED_VARIABLE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = "// Copyright © 2024 Kartik Venugopal. All rights reserved.\n//\n// This software is licensed under the MIT software license.\n// See the file \"LICENSE\" in the project root directory for license terms.\n//"; MACOSX_DEPLOYMENT_TARGET = 10.12; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; @@ -6793,6 +6799,7 @@ INFOPLIST_FILE = Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "Aural Player"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.music"; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", @@ -6830,6 +6837,7 @@ INFOPLIST_FILE = Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "Aural Player"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.music"; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", diff --git a/Aural.xcodeproj/project.xcworkspace/xcuserdata/kven.xcuserdatad/UserInterfaceState.xcuserstate b/Aural.xcodeproj/project.xcworkspace/xcuserdata/kven.xcuserdatad/UserInterfaceState.xcuserstate index 3bb30c040..30899517e 100644 Binary files a/Aural.xcodeproj/project.xcworkspace/xcuserdata/kven.xcuserdatad/UserInterfaceState.xcuserstate and b/Aural.xcodeproj/project.xcworkspace/xcuserdata/kven.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/Aural.xcodeproj/xcuserdata/kven.xcuserdatad/IDETemplateMacros.plist b/Aural.xcodeproj/xcuserdata/kven.xcuserdatad/IDETemplateMacros.plist index d736a6776..525ac3077 100644 --- a/Aural.xcodeproj/xcuserdata/kven.xcuserdatad/IDETemplateMacros.plist +++ b/Aural.xcodeproj/xcuserdata/kven.xcuserdatad/IDETemplateMacros.plist @@ -2,15 +2,15 @@ - FILEHEADER - -// ___FILENAME___ -// ___PACKAGENAME___ -// -// Copyright © 2021 Kartik Venugopal. All rights reserved. -// -// This software is licensed under the MIT software license. -// See the file "LICENSE" in the project root directory for license terms. -// + FILEHEADER + +// ___FILENAME___ +// Aural +// +// Copyright © ___YEAR___ Kartik Venugopal. All rights reserved. +// +// This software is licensed under the MIT software license. +// See the file "LICENSE" in the project root directory for license terms. +// - \ No newline at end of file + diff --git a/Source/AppDelegate+Init.swift b/Source/AppDelegate+Init.swift index 90185a811..a09eef735 100644 --- a/Source/AppDelegate+Init.swift +++ b/Source/AppDelegate+Init.swift @@ -104,12 +104,16 @@ extension AppDelegate { // (they are not referred to in code that is executed on app startup). // _ = libraryDelegate - _ = mediaKeyHandler - _ = remoteControlManager + eagerlyInitializeObjects(mediaKeyHandler, remoteControlManager, replayGainScanner) WaveformView.initializeImageCache() } + /// + /// Does nothing ... simply referencing objects in the caller will cause them to be eagerly initialized. + /// + func eagerlyInitializeObjects(_ object: Any...) {} + func beginPeriodicPersistence() { persistenceTaskExecutor = RepeatingTaskExecutor(intervalMillis: Self.persistenceTaskInterval * 1000, diff --git a/Source/AppDelegate.swift b/Source/AppDelegate.swift index 1d406288b..edb84dc01 100644 --- a/Source/AppDelegate.swift +++ b/Source/AppDelegate.swift @@ -61,7 +61,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { func applicationDidFinishLaunching(_ aNotification: Notification) { // Force eager loading of persistent state - _ = appPersistentState + eagerlyInitializeObjects(appPersistentState) if appSetup.setupRequired { performAppSetup() @@ -79,7 +79,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { // fontSchemesManager.printNumObservers() // } } - + /// Opens the application with a single file (audio file or playlist) public func application(_ sender: NSApplication, openFile filename: String) -> Bool { diff --git a/Source/Core/AudioGraph/CustomNodes/ReplayGainNode.swift b/Source/Core/AudioGraph/CustomNodes/ReplayGainNode.swift index 389d11408..7e0af6188 100644 --- a/Source/Core/AudioGraph/CustomNodes/ReplayGainNode.swift +++ b/Source/Core/AudioGraph/CustomNodes/ReplayGainNode.swift @@ -2,7 +2,7 @@ // ReplayGainNode.swift // Aural // -// Copyright © 2021 Kartik Venugopal. All rights reserved. +// Copyright © 2024 Kartik Venugopal. All rights reserved. // // This software is licensed under the MIT software license. // See the file "LICENSE" in the project root directory for license terms. diff --git a/Source/Core/AudioGraph/EffectsUnits/PresetsAndProfiles/ReplayGainPresets.swift b/Source/Core/AudioGraph/EffectsUnits/PresetsAndProfiles/ReplayGainPresets.swift index 78b7995d1..d72e02efd 100644 --- a/Source/Core/AudioGraph/EffectsUnits/PresetsAndProfiles/ReplayGainPresets.swift +++ b/Source/Core/AudioGraph/EffectsUnits/PresetsAndProfiles/ReplayGainPresets.swift @@ -2,7 +2,7 @@ // ReplayGainPresets.swift // Aural // -// Copyright © 2021 Kartik Venugopal. All rights reserved. +// Copyright © 2024 Kartik Venugopal. All rights reserved. // // This software is licensed under the MIT software license. // See the file "LICENSE" in the project root directory for license terms. diff --git a/Source/Core/AudioGraph/EffectsUnits/ReplayGain/LoudnessScan/AVFReplayGainScanner.swift b/Source/Core/AudioGraph/EffectsUnits/ReplayGain/LoudnessScan/AVFReplayGainScanner.swift index d9f655dc2..5764c77cd 100644 --- a/Source/Core/AudioGraph/EffectsUnits/ReplayGain/LoudnessScan/AVFReplayGainScanner.swift +++ b/Source/Core/AudioGraph/EffectsUnits/ReplayGain/LoudnessScan/AVFReplayGainScanner.swift @@ -44,7 +44,7 @@ class AVFReplayGainScanner: EBUR128LoudnessScannerProtocol { self.channelCount = audioFormat.channelCount self.sampleRate = audioFormat.sampleRate - ebur128 = try EBUR128State(channelCount: Int(channelCount), sampleRate: Int(sampleRate), mode: .samplePeak) + ebur128 = try EBUR128State(file: file, channelCount: Int(channelCount), sampleRate: Int(sampleRate), mode: .samplePeak) self.channelLayout = audioFormat.channelLayout ?? .defaultLayoutForChannelCount(channelCount) diff --git a/Source/Core/AudioGraph/EffectsUnits/ReplayGain/LoudnessScan/FFmpegReplayGainScanner.swift b/Source/Core/AudioGraph/EffectsUnits/ReplayGain/LoudnessScan/FFmpegReplayGainScanner.swift index 3ba74d0b6..7874611cc 100644 --- a/Source/Core/AudioGraph/EffectsUnits/ReplayGain/LoudnessScan/FFmpegReplayGainScanner.swift +++ b/Source/Core/AudioGraph/EffectsUnits/ReplayGain/LoudnessScan/FFmpegReplayGainScanner.swift @@ -50,7 +50,7 @@ class FFmpegReplayGainScanner: EBUR128LoudnessScannerProtocol { sampleFormat = codec.sampleFormat.avFormat sampleRate = Int(codec.sampleRate) - ebur128 = try EBUR128State(channelCount: channelCount, sampleRate: sampleRate, mode: .samplePeak) + ebur128 = try EBUR128State(file: file, channelCount: channelCount, sampleRate: sampleRate, mode: .samplePeak) self.targetFormat = sampleFormat diff --git a/Source/Core/AudioGraph/EffectsUnits/ReplayGain/LoudnessScan/ReplayGainAlbumScannerOperation.swift b/Source/Core/AudioGraph/EffectsUnits/ReplayGain/LoudnessScan/ReplayGainAlbumScannerOperation.swift index 5c7fae0ec..e4d61d286 100644 --- a/Source/Core/AudioGraph/EffectsUnits/ReplayGain/LoudnessScan/ReplayGainAlbumScannerOperation.swift +++ b/Source/Core/AudioGraph/EffectsUnits/ReplayGain/LoudnessScan/ReplayGainAlbumScannerOperation.swift @@ -2,7 +2,7 @@ // ReplayGainAlbumScannerOperation.swift // Aural // -// Copyright © 2021 Kartik Venugopal. All rights reserved. +// Copyright © 2024 Kartik Venugopal. All rights reserved. // // This software is licensed under the MIT software license. // See the file "LICENSE" in the project root directory for license terms. @@ -12,6 +12,8 @@ import Foundation class ReplayGainAlbumScannerOperation: Operation { + static let queue: OperationQueue = .init(opCount: System.numberOfActiveCores, qos: .userInitiated) + let files: [URL] private var scanners: [EBUR128LoudnessScannerProtocol] = [] @@ -34,7 +36,7 @@ class ReplayGainAlbumScannerOperation: Operation { private var _isFinished = false override var isFinished: Bool {_isFinished} - init(files: [URL], completionHandler: @escaping (ReplayGainAlbumScannerOperation, EBUR128AlbumAnalysisResult?) -> Void) throws { + init(files: [URL], completionHandler: @escaping (ReplayGainAlbumScannerOperation, EBUR128AlbumAnalysisResult?) -> Void) { self.files = files self.completionHandler = completionHandler @@ -67,6 +69,8 @@ class ReplayGainAlbumScannerOperation: Operation { // Do nothing if any of these flags is set. guard !isExecuting, !isFinished, !isCancelled else {return} + Self.queue.addOperations(dependencies, waitUntilFinished: true) + // Update state for KVO. willChangeValue(forKey: "isExecuting") _isExecuting = true @@ -74,6 +78,8 @@ class ReplayGainAlbumScannerOperation: Operation { var result: EBUR128AlbumAnalysisResult? = nil + print("Starting album computation ... \(eburs.count) eburs, \(results.count) results") + do { result = try EBUR128State.computeAlbumLoudnessAndPeak(with: eburs.array, andTrackResults: results.array) } catch { diff --git a/Source/Core/AudioGraph/EffectsUnits/ReplayGain/LoudnessScan/ReplayGainScanner.swift b/Source/Core/AudioGraph/EffectsUnits/ReplayGain/LoudnessScan/ReplayGainScanner.swift index 8f5623e76..bfbdff94e 100644 --- a/Source/Core/AudioGraph/EffectsUnits/ReplayGain/LoudnessScan/ReplayGainScanner.swift +++ b/Source/Core/AudioGraph/EffectsUnits/ReplayGain/LoudnessScan/ReplayGainScanner.swift @@ -2,7 +2,7 @@ // ReplayGainScanner.swift // Aural // -// Copyright © 2021 Kartik Venugopal. All rights reserved. +// Copyright © 2024 Kartik Venugopal. All rights reserved. // // This software is licensed under the MIT software license. // See the file "LICENSE" in the project root directory for license terms. @@ -25,32 +25,43 @@ protocol EBUR128LoudnessScannerProtocol { var isCancelled: Bool {get} } +typealias ReplayGainScanCompletionHandler = (ReplayGain?) -> Void + class ReplayGainScanner { - let cache: ConcurrentMap = ConcurrentMap() + let trackGainCache: ConcurrentMap = ConcurrentMap() + let albumGainCache: ConcurrentMap = ConcurrentMap() - private var scanOp: ReplayGainTrackScannerOperation? = nil + private var scanOp: Operation? = nil init(persistentState: ReplayGainAnalysisCachePersistentState?) { - guard let cache = persistentState?.cache else {return} + if let trackGainCache = persistentState?.trackGainCache { + + for (file, result) in trackGainCache { + self.trackGainCache[file] = result + } + } - for (file, result) in cache { - self.cache[file] = result + if let albumGainCache = persistentState?.albumGainCache { + + for (albumName, result) in albumGainCache { + self.albumGainCache[albumName] = result + } } - print("ReplayGainScanner.init() read \(self.cache.count) cache entries") + print("ReplayGainScanner.init() read \(self.trackGainCache.count) trackGain cache entries and \(self.albumGainCache.count) albumGain cache entries.") } - func scan(forFile file: URL, _ completionHandler: @escaping (ReplayGain?) -> Void) { + func scanTrack(file: URL, _ completionHandler: @escaping ReplayGainScanCompletionHandler) { cancelOngoingScan() // First, check the cache - if let theResult = cache[file] { + if let theResult = trackGainCache[file] { // Cache hit - completionHandler(ReplayGain(ebur128AnalysisResult: theResult)) + completionHandler(theResult) return } @@ -65,8 +76,60 @@ class ReplayGainScanner { if let theResult = ebur128Result { // Scan succeeded, cache the result - self?.cache[file] = theResult - completionHandler(ReplayGain(ebur128AnalysisResult: theResult)) + let replayGain = ReplayGain(ebur128TrackAnalysisResult: theResult) + self?.trackGainCache[file] = replayGain + completionHandler(replayGain) + + } else { + + // Scan failed + completionHandler(nil) + } + + self?.scanOp = nil + } + + scanOp?.start() + } + + func scanAlbum(named albumName: String, withFiles files: [URL], forFile file: URL, _ completionHandler: @escaping ReplayGainScanCompletionHandler) { + + cancelOngoingScan() + + // First, check the cache + if let theResult = albumGainCache[albumName], theResult.containsResultsForAllFiles(files), let trackResult = trackGainCache[file] { + + // Cache hit + print("Album cache hit !!!") + completionHandler(trackResult) + return + } + + // Cache miss, initiate a scan + + print("\nScanning album '\(albumName)' with \(files.count) files: \(files.map {$0.lastPathComponent}) ...") + + scanOp = ReplayGainAlbumScannerOperation(files: files) {[weak self] finishedScanOp, ebur128Result in + + // A previously scheduled scan op may finish just before being cancelled. This check + // will prevent rogue completion handler execution. + guard self?.scanOp == finishedScanOp else {return} + + if let theAlbumResult = ebur128Result { + + for (trackFile, trackResult) in theAlbumResult.trackResults { + + self?.trackGainCache[trackFile] = ReplayGain(ebur128TrackAnalysisResult: trackResult, + ebur128AlbumAnalysisResult: theAlbumResult) + } + + // Scan succeeded, cache the result + self?.albumGainCache[albumName] = AlbumReplayGain(albumName: albumName, files: files, + loudness: theAlbumResult.albumLoudness, + replayGain: theAlbumResult.albumReplayGain, + peak: theAlbumResult.albumPeak) + + completionHandler(self?.trackGainCache[file]) } else { @@ -87,6 +150,6 @@ class ReplayGainScanner { } var persistentState: ReplayGainAnalysisCachePersistentState { - .init(cache: cache.map) + .init(trackGainCache: trackGainCache.map, albumGainCache: albumGainCache.map) } } diff --git a/Source/Core/AudioGraph/EffectsUnits/ReplayGain/LoudnessScan/ReplayGainTrackScannerOperation.swift b/Source/Core/AudioGraph/EffectsUnits/ReplayGain/LoudnessScan/ReplayGainTrackScannerOperation.swift index 65d92b4bf..21ebe8dc3 100644 --- a/Source/Core/AudioGraph/EffectsUnits/ReplayGain/LoudnessScan/ReplayGainTrackScannerOperation.swift +++ b/Source/Core/AudioGraph/EffectsUnits/ReplayGain/LoudnessScan/ReplayGainTrackScannerOperation.swift @@ -2,7 +2,7 @@ // ReplayGainScannerOperation.swift // Aural // -// Copyright © 2021 Kartik Venugopal. All rights reserved. +// Copyright © 2024 Kartik Venugopal. All rights reserved. // // This software is licensed under the MIT software license. // See the file "LICENSE" in the project root directory for license terms. diff --git a/Source/Core/AudioGraph/EffectsUnits/ReplayGain/ReplayGainUnit.swift b/Source/Core/AudioGraph/EffectsUnits/ReplayGain/ReplayGainUnit.swift index f0e45bbb9..87b6e3588 100644 --- a/Source/Core/AudioGraph/EffectsUnits/ReplayGain/ReplayGainUnit.swift +++ b/Source/Core/AudioGraph/EffectsUnits/ReplayGain/ReplayGainUnit.swift @@ -2,7 +2,7 @@ // ReplayGainUnit.swift // Aural // -// Copyright © 2021 Kartik Venugopal. All rights reserved. +// Copyright © 2024 Kartik Venugopal. All rights reserved. // // This software is licensed under the MIT software license. // See the file "LICENSE" in the project root directory for license terms. diff --git a/Source/Core/AudioGraph/EffectsUnits/ReplayGain/ReplayGainUnitDelegate.swift b/Source/Core/AudioGraph/EffectsUnits/ReplayGain/ReplayGainUnitDelegate.swift index 3b11770df..6db1e6898 100644 --- a/Source/Core/AudioGraph/EffectsUnits/ReplayGain/ReplayGainUnitDelegate.swift +++ b/Source/Core/AudioGraph/EffectsUnits/ReplayGain/ReplayGainUnitDelegate.swift @@ -2,7 +2,7 @@ // ReplayGainUnitDelegate.swift // Aural // -// Copyright © 2021 Kartik Venugopal. All rights reserved. +// Copyright © 2024 Kartik Venugopal. All rights reserved. // // This software is licensed under the MIT software license. // See the file "LICENSE" in the project root directory for license terms. @@ -91,7 +91,7 @@ class ReplayGainUnitDelegate: EffectsUnitDelegate, ReplayGainUni case .metadataOrAnalysis: - if let replayGain = theTrack.replayGain { + if let replayGain = theTrack.replayGain, hasEnoughInfo(replayGain: replayGain) { // Has metadata unit.replayGain = replayGain @@ -102,23 +102,53 @@ class ReplayGainUnitDelegate: EffectsUnitDelegate, ReplayGainUni unit.replayGain = nil // Analyze - analyze(file: theTrack.file) + analyze(track: theTrack) } case .metadataOnly: unit.replayGain = theTrack.replayGain case .analysisOnly: - analyze(file: theTrack.file) + analyze(track: theTrack) } } - private func analyze(file: URL) { + private func hasEnoughInfo(replayGain: ReplayGain) -> Bool { - _isScanning.setTrue() - Messenger.publish(.Effects.ReplayGainUnit.scanInitiated) + switch unit.mode { + + case .preferAlbumGain: + + if preventClipping { + return replayGain.albumGain != nil ? replayGain.albumPeak != nil : replayGain.trackPeak != nil + } + + return true + + case .preferTrackGain: + + if preventClipping { + return replayGain.trackGain != nil ? replayGain.trackPeak != nil : replayGain.albumPeak != nil + } + + return true + + case .trackGainOnly: + return replayGain.trackGain != nil && (preventClipping ? replayGain.trackPeak != nil : true) + } + } + + private func analyze(track: Track) { - replayGainScanner.scan(forFile: file) {[weak self] (replayGain: ReplayGain?) in + let file = track.file + + func beganScanning() { + + _isScanning.setTrue() + Messenger.publish(.Effects.ReplayGainUnit.scanInitiated) + } + + let completionHandler: ReplayGainScanCompletionHandler = {[weak self] (replayGain: ReplayGain?) in guard let strongSelf = self else {return} @@ -127,5 +157,25 @@ class ReplayGainUnitDelegate: EffectsUnitDelegate, ReplayGainUni Messenger.publish(.Effects.ReplayGainUnit.scanCompleted) } + + DispatchQueue.global(qos: .userInitiated).async { + + switch self.unit.mode { + + case .preferAlbumGain: + + if let albumName = track.album { + + beganScanning() + let albumFiles = playQueueDelegate.tracks.filter {$0.album == albumName}.map {$0.file} + replayGainScanner.scanAlbum(named: albumName, withFiles: albumFiles, forFile: track.file, completionHandler) + } + + case .preferTrackGain, .trackGainOnly: + + beganScanning() + replayGainScanner.scanTrack(file: file, completionHandler) + } + } } } diff --git a/Source/Core/AudioGraph/EffectsUnits/ReplayGain/ReplayGainUnitDelegateProtocol.swift b/Source/Core/AudioGraph/EffectsUnits/ReplayGain/ReplayGainUnitDelegateProtocol.swift index 6a6bb7386..047b5c2e4 100644 --- a/Source/Core/AudioGraph/EffectsUnits/ReplayGain/ReplayGainUnitDelegateProtocol.swift +++ b/Source/Core/AudioGraph/EffectsUnits/ReplayGain/ReplayGainUnitDelegateProtocol.swift @@ -2,7 +2,7 @@ // ReplayGainUnitDelegateProtocol.swift // Aural // -// Copyright © 2021 Kartik Venugopal. All rights reserved. +// Copyright © 2024 Kartik Venugopal. All rights reserved. // // This software is licensed under the MIT software license. // See the file "LICENSE" in the project root directory for license terms. diff --git a/Source/Core/AudioGraph/EffectsUnits/ReplayGain/ReplayGainUnitProtocol.swift b/Source/Core/AudioGraph/EffectsUnits/ReplayGain/ReplayGainUnitProtocol.swift index dd4910754..2e8eeb647 100644 --- a/Source/Core/AudioGraph/EffectsUnits/ReplayGain/ReplayGainUnitProtocol.swift +++ b/Source/Core/AudioGraph/EffectsUnits/ReplayGain/ReplayGainUnitProtocol.swift @@ -2,7 +2,7 @@ // ReplayGainUnitProtocol.swift // Aural // -// Copyright © 2021 Kartik Venugopal. All rights reserved. +// Copyright © 2024 Kartik Venugopal. All rights reserved. // // This software is licensed under the MIT software license. // See the file "LICENSE" in the project root directory for license terms. diff --git a/Source/Core/EBUR128/EBUR128State.swift b/Source/Core/EBUR128/EBUR128State.swift index df4611362..c4d821cda 100644 --- a/Source/Core/EBUR128/EBUR128State.swift +++ b/Source/Core/EBUR128/EBUR128State.swift @@ -14,6 +14,8 @@ fileprivate typealias PeakAnalysisFunction = (UnsafeMutablePointer var ebur128: ebur128_state { @@ -27,7 +29,9 @@ class EBUR128State { static let targetLoudness: Double = -18 static let assumedPeak: Double = 1 - init(channelCount: Int, sampleRate: Int, mode: EBUR128Mode) throws { + init(file: URL, channelCount: Int, sampleRate: Int, mode: EBUR128Mode) throws { + + self.file = file self.channelCount = channelCount self.sampleRate = sampleRate @@ -82,7 +86,7 @@ class EBUR128State { let peak = try computePeak() let replayGain = Self.targetLoudness - loudness - return EBUR128TrackAnalysisResult(loudness: loudness, peak: peak, replayGain: replayGain) + return EBUR128TrackAnalysisResult(file: self.file, loudness: loudness, peak: peak, replayGain: replayGain) } private func computeLoudness() throws -> Double { @@ -127,10 +131,16 @@ class EBUR128State { ebur128_loudness_global_multiple(&pointers, eburs.count, &albumLoudness) let albumPeak = trackResults.map {$0.peak}.max() ?? Self.assumedPeak + var results: [URL: EBUR128TrackAnalysisResult] = [:] + + for result in trackResults { + results[result.file] = result + } + return EBUR128AlbumAnalysisResult(albumLoudness: albumLoudness, albumPeak: albumPeak, albumReplayGain: Self.targetLoudness - albumLoudness, - trackResults: trackResults) + trackResults: results) } deinit { @@ -156,6 +166,8 @@ enum EBUR128Mode { struct EBUR128TrackAnalysisResult: Codable { + let file: URL + let loudness: Double let peak: Double let replayGain: Double @@ -167,5 +179,5 @@ struct EBUR128AlbumAnalysisResult: Codable { let albumPeak: Double let albumReplayGain: Double - let trackResults: [EBUR128TrackAnalysisResult] + let trackResults: [URL: EBUR128TrackAnalysisResult] } diff --git a/Source/Core/LibCue/LibCueMapper.swift b/Source/Core/LibCue/LibCueMapper.swift index 98386050f..ea9e1fdde 100644 --- a/Source/Core/LibCue/LibCueMapper.swift +++ b/Source/Core/LibCue/LibCueMapper.swift @@ -2,7 +2,7 @@ // LibCueMapper.swift // Aural // -// Copyright © 2021 Kartik Venugopal. All rights reserved. +// Copyright © 2024 Kartik Venugopal. All rights reserved. // // This software is licensed under the MIT software license. // See the file "LICENSE" in the project root directory for license terms. diff --git a/Source/Core/LibCue/Support/LibCueErrors.swift b/Source/Core/LibCue/Support/LibCueErrors.swift index c305fade4..4ec91b788 100644 --- a/Source/Core/LibCue/Support/LibCueErrors.swift +++ b/Source/Core/LibCue/Support/LibCueErrors.swift @@ -2,7 +2,7 @@ // LibCueErrors.swift // Aural // -// Copyright © 2021 Kartik Venugopal. All rights reserved. +// Copyright © 2024 Kartik Venugopal. All rights reserved. // // This software is licensed under the MIT software license. // See the file "LICENSE" in the project root directory for license terms. diff --git a/Source/Core/Persistence/Model/AudioGraph/ReplayGainUnitPersistentState.swift b/Source/Core/Persistence/Model/AudioGraph/ReplayGainUnitPersistentState.swift index e26c22458..d62877c3b 100644 --- a/Source/Core/Persistence/Model/AudioGraph/ReplayGainUnitPersistentState.swift +++ b/Source/Core/Persistence/Model/AudioGraph/ReplayGainUnitPersistentState.swift @@ -2,7 +2,7 @@ // ReplayGainUnitPersistentState.swift // Aural // -// Copyright © 2021 Kartik Venugopal. All rights reserved. +// Copyright © 2024 Kartik Venugopal. All rights reserved. // // This software is licensed under the MIT software license. // See the file "LICENSE" in the project root directory for license terms. @@ -62,5 +62,6 @@ struct ReplayGainPresetPersistentState: Codable { struct ReplayGainAnalysisCachePersistentState: Codable { - let cache: [URL: EBUR128TrackAnalysisResult]? + let trackGainCache: [URL: ReplayGain]? + let albumGainCache: [String: AlbumReplayGain]? } diff --git a/Source/Core/TrackIO/Model/AlbumReplayGain.swift b/Source/Core/TrackIO/Model/AlbumReplayGain.swift new file mode 100644 index 000000000..fbf91d132 --- /dev/null +++ b/Source/Core/TrackIO/Model/AlbumReplayGain.swift @@ -0,0 +1,35 @@ +// +// AlbumReplayGain.swift +// Aural +// +// Copyright © 2024 Kartik Venugopal. All rights reserved. +// +// This software is licensed under the MIT software license. +// See the file "LICENSE" in the project root directory for license terms. +// + +import Foundation + +struct AlbumReplayGain: Codable { + + let albumName: String + let files: Set + + let loudness: Double + let replayGain: Double + let peak: Double + + init(albumName: String, files: [URL], loudness: Double, replayGain: Double, peak: Double) { + + self.albumName = albumName + self.files = Set(files) + + self.loudness = loudness + self.replayGain = replayGain + self.peak = peak + } + + func containsResultsForAllFiles(_ targetFiles: [URL]) -> Bool { + self.files.isSuperset(of: targetFiles) + } +} diff --git a/Source/Core/TrackIO/Model/ReplayGain.swift b/Source/Core/TrackIO/Model/ReplayGain.swift index 0cf92b6be..836a54d55 100644 --- a/Source/Core/TrackIO/Model/ReplayGain.swift +++ b/Source/Core/TrackIO/Model/ReplayGain.swift @@ -65,12 +65,21 @@ struct ReplayGain: Codable { } } - init(ebur128AnalysisResult: EBUR128TrackAnalysisResult) { + init(ebur128TrackAnalysisResult: EBUR128TrackAnalysisResult, ebur128AlbumAnalysisResult: EBUR128AlbumAnalysisResult? = nil) { - self.trackGain = Float(ebur128AnalysisResult.replayGain) - self.trackPeak = Float(ebur128AnalysisResult.peak) + self.trackGain = Float(ebur128TrackAnalysisResult.replayGain) + self.trackPeak = Float(ebur128TrackAnalysisResult.peak) - self.albumGain = nil - self.albumPeak = nil + if let albumReplayGain = ebur128AlbumAnalysisResult?.albumReplayGain { + self.albumGain = Float(albumReplayGain) + } else { + self.albumGain = nil + } + + if let albumPeak = ebur128AlbumAnalysisResult?.albumPeak { + self.albumPeak = Float(albumPeak) + } else { + self.albumPeak = nil + } } } diff --git a/Source/UI/Effects/ReplayGain/DecibelSelector/DecibelSelectorView.swift b/Source/UI/Effects/ReplayGain/DecibelSelector/DecibelSelectorView.swift index 84b034e90..c82cc9434 100644 --- a/Source/UI/Effects/ReplayGain/DecibelSelector/DecibelSelectorView.swift +++ b/Source/UI/Effects/ReplayGain/DecibelSelector/DecibelSelectorView.swift @@ -2,7 +2,7 @@ // DecibelSelectorView.swift // Aural // -// Copyright © 2021 Kartik Venugopal. All rights reserved. +// Copyright © 2024 Kartik Venugopal. All rights reserved. // // This software is licensed under the MIT software license. // See the file "LICENSE" in the project root directory for license terms. diff --git a/Source/UI/Effects/ReplayGain/ReplayGainUnitViewController.swift b/Source/UI/Effects/ReplayGain/ReplayGainUnitViewController.swift index 4dd34d15a..c8e6a580c 100644 --- a/Source/UI/Effects/ReplayGain/ReplayGainUnitViewController.swift +++ b/Source/UI/Effects/ReplayGain/ReplayGainUnitViewController.swift @@ -2,7 +2,7 @@ // ReplayGainUnitViewController.swift // Aural // -// Copyright © 2021 Kartik Venugopal. All rights reserved. +// Copyright © 2024 Kartik Venugopal. All rights reserved. // // This software is licensed under the MIT software license. // See the file "LICENSE" in the project root directory for license terms. @@ -119,7 +119,7 @@ class ReplayGainUnitViewController: EffectsUnitViewController { } private func scanInitiated() { - lblGain.stringValue = "Analyzing track loudness ..." + lblGain.stringValue = "Analyzing \(replayGainUnit.mode == .preferAlbumGain ? "album" : "track") loudness ..." } override func fontSchemeChanged() { diff --git a/Source/UI/PlayQueue/SimpleView/PlayQueueSimpleViewController.swift b/Source/UI/PlayQueue/SimpleView/PlayQueueSimpleViewController.swift index df8b94ebd..b01ea0bd4 100644 --- a/Source/UI/PlayQueue/SimpleView/PlayQueueSimpleViewController.swift +++ b/Source/UI/PlayQueue/SimpleView/PlayQueueSimpleViewController.swift @@ -105,9 +105,9 @@ class AttrCellView: NSTableCellView { func update(artist: String, title: String) { - let muthu = "\(artist) ".attributed(font: systemFontScheme.normalFont, color: systemColorScheme.secondaryTextColor) + title.attributed(font: systemFontScheme.normalFont, color: systemColorScheme.primaryTextColor) + let muthu = "\(artist) ".attributed(font: systemFontScheme.normalFont, color: systemColorScheme.secondaryTextColor) + title.attributed(font: systemFontScheme.normalFont, color: systemColorScheme.primaryTextColor) - let selMuthu = "\(artist) ".attributed(font: systemFontScheme.normalFont, color: systemColorScheme.secondarySelectedTextColor) + title.attributed(font: systemFontScheme.normalFont, color: systemColorScheme.primarySelectedTextColor) + let selMuthu = "\(artist) ".attributed(font: systemFontScheme.normalFont, color: systemColorScheme.secondarySelectedTextColor) + title.attributed(font: systemFontScheme.normalFont, color: systemColorScheme.primarySelectedTextColor) let style: NSMutableParagraphStyle = NSMutableParagraphStyle() style.lineBreakMode = .byTruncatingTail