Skip to content

Commit

Permalink
#12 Album Gain
Browse files Browse the repository at this point in the history
  • Loading branch information
kartik-venugopal committed Aug 24, 2024
1 parent 71bb926 commit ca414c1
Show file tree
Hide file tree
Showing 25 changed files with 252 additions and 64 deletions.
8 changes: 8 additions & 0 deletions Aural.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -1928,6 +1929,7 @@
3E7E9F4B2C66B06F0011DE8E /* WaveformViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaveformViewController.swift; sourceTree = "<group>"; };
3E7E9F4D2C66B1AF0011DE8E /* WaveformWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaveformWindowController.swift; sourceTree = "<group>"; };
3E804D212C29F7840049AC27 /* GestureHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GestureHandler.swift; sourceTree = "<group>"; };
3E8145C52C7A3850005BA9B9 /* AlbumReplayGain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbumReplayGain.swift; sourceTree = "<group>"; };
3E836E322868964D009371D4 /* PlayQueueContainer.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = PlayQueueContainer.xib; sourceTree = "<group>"; };
3E836E3A2868B9FD009371D4 /* UnifiedPlayerWindowController+EventHandling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UnifiedPlayerWindowController+EventHandling.swift"; sourceTree = "<group>"; };
3E86B5062C234A420097746A /* buildingLibrary.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = buildingLibrary.gif; sourceTree = "<group>"; };
Expand Down Expand Up @@ -3015,6 +3017,7 @@
3E02175B2C23490E00865AC2 /* MetadataType.swift */,
3E02175C2C23490E00865AC2 /* PrimaryMetadata.swift */,
3E5701952C6EB16A007B8611 /* ReplayGain.swift */,
3E8145C52C7A3850005BA9B9 /* AlbumReplayGain.swift */,
3E02175D2C23490E00865AC2 /* Track.swift */,
);
path = Model;
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
Binary file not shown.
22 changes: 11 additions & 11 deletions Aural.xcodeproj/xcuserdata/kven.xcuserdatad/IDETemplateMacros.plist
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>FILEHEADER</key>
<string>
// ___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.
// </string>
<key>FILEHEADER</key>
<string>
// ___FILENAME___
// Aural
//
// Copyright © ___YEAR___ Kartik Venugopal. All rights reserved.
//
// This software is licensed under the MIT software license.
// See the file &quot;LICENSE&quot; in the project root directory for license terms.
//</string>
</dict>
</plist>
</plist>
8 changes: 6 additions & 2 deletions Source/AppDelegate+Init.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions Source/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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 {

Expand Down
2 changes: 1 addition & 1 deletion Source/Core/AudioGraph/CustomNodes/ReplayGainNode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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] = []
Expand All @@ -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
Expand Down Expand Up @@ -67,13 +69,17 @@ 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
didChangeValue(forKey: "isExecuting")

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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -25,32 +25,43 @@ protocol EBUR128LoudnessScannerProtocol {
var isCancelled: Bool {get}
}

typealias ReplayGainScanCompletionHandler = (ReplayGain?) -> Void

class ReplayGainScanner {

let cache: ConcurrentMap<URL, EBUR128TrackAnalysisResult> = ConcurrentMap()
let trackGainCache: ConcurrentMap<URL, ReplayGain> = ConcurrentMap()
let albumGainCache: ConcurrentMap<String, AlbumReplayGain> = 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
}

Expand All @@ -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 {

Expand All @@ -87,6 +150,6 @@ class ReplayGainScanner {
}

var persistentState: ReplayGainAnalysisCachePersistentState {
.init(cache: cache.map)
.init(trackGainCache: trackGainCache.map, albumGainCache: albumGainCache.map)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading

0 comments on commit ca414c1

Please sign in to comment.