diff --git a/Aural.xcodeproj/project.xcworkspace/xcuserdata/kven.xcuserdatad/UserInterfaceState.xcuserstate b/Aural.xcodeproj/project.xcworkspace/xcuserdata/kven.xcuserdatad/UserInterfaceState.xcuserstate index 47c371001..fcc9b4435 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/Source/Core/AudioGraph/AudioGraph.swift b/Source/Core/AudioGraph/AudioGraph.swift index a4c822f86..aad8a38ee 100644 --- a/Source/Core/AudioGraph/AudioGraph.swift +++ b/Source/Core/AudioGraph/AudioGraph.swift @@ -275,7 +275,8 @@ class AudioGraph: AudioGraphProtocol, PersistentModelObject { replayGainUnit: replayGainUnit.persistentState, audioUnits: audioUnits.map {$0.persistentState}, audioUnitPresets: audioUnitPresets.persistentState, - soundProfiles: soundProfiles.persistentState) + soundProfiles: soundProfiles.persistentState, + replayGainAnalysisCache: replayGainScanner.persistentState) } } diff --git a/Source/Core/AudioGraph/AudioGraphDelegate.swift b/Source/Core/AudioGraph/AudioGraphDelegate.swift index d6e2260d2..876eae1a9 100644 --- a/Source/Core/AudioGraph/AudioGraphDelegate.swift +++ b/Source/Core/AudioGraph/AudioGraphDelegate.swift @@ -275,15 +275,7 @@ class AudioGraphDelegate: AudioGraphDelegateProtocol { } // Replay gain ------------------------------------------------------------ - - // TODO: Only do this if using metadata -// print("Applying replay gain: \(newTrack?.playbackContext?.replayGain?.trackGain) for new track: \(newTrack)") -// replayGainUnit.applyGain(newTrack?.playbackContext?.replayGain) - replayGainUnit.applyGain(nil) - - if let newTrackFile = newTrack?.file { - replayGainUnit.initiateScan(forFile: newTrackFile) - } + replayGainUnit.applyReplayGain(forTrack: newTrack) } @inline(__always) diff --git a/Source/Core/AudioGraph/EffectsUnits/ReplayGain/LoudnessScan/ReplayGainScanner.swift b/Source/Core/AudioGraph/EffectsUnits/ReplayGain/LoudnessScan/ReplayGainScanner.swift index b22346ad1..7f69aa8f5 100644 --- a/Source/Core/AudioGraph/EffectsUnits/ReplayGain/LoudnessScan/ReplayGainScanner.swift +++ b/Source/Core/AudioGraph/EffectsUnits/ReplayGain/LoudnessScan/ReplayGainScanner.swift @@ -19,26 +19,55 @@ protocol EBUR128LoudnessScannerProtocol { class ReplayGainScanner { - let file: URL - let scanner: EBUR128LoudnessScannerProtocol + let cache: ConcurrentMap = ConcurrentMap() - init(file: URL) throws { + init(persistentState: ReplayGainAnalysisCachePersistentState?) { - self.file = file - scanner = file.isNativelySupported ? try AVFReplayGainScanner(file: file) : try FFmpegReplayGainScanner(file: file) + guard let cache = persistentState?.cache else {return} + + for (file, result) in cache { + self.cache[file] = result + } + + print("ReplayGainScanner.init() read \(self.cache.count) cache entries") } - func scan(_ completionHandler: @escaping (ReplayGain?) -> Void) { + func scan(forFile file: URL, _ completionHandler: @escaping (ReplayGain?) -> Void) throws { + + // First, check the cache + if let theResult = cache[file] { + + // Cache hit + print("ReplayGainScanner.init() CACHE HIT !!! \(theResult.replayGain) for file \(file.lastPathComponent)") + completionHandler(ReplayGain(ebur128AnalysisResult: theResult)) + return + } + + print("ReplayGainScanner.init() CACHE MISS for file \(file.lastPathComponent)") + + // Cache miss, initiate a scan - lazy var filePath = file.path + let scanner: EBUR128LoudnessScannerProtocol = file.isNativelySupported ? + try AVFReplayGainScanner(file: file) : + try FFmpegReplayGainScanner(file: file) - scanner.scan {ebur128Result in + scanner.scan {[weak self] ebur128Result in if let theResult = ebur128Result { + + // Scan succeeded, cache the result + self?.cache[file] = theResult completionHandler(ReplayGain(ebur128AnalysisResult: theResult)) + } else { + + // Scan failed completionHandler(nil) } } } + + var persistentState: ReplayGainAnalysisCachePersistentState { + .init(cache: cache.map) + } } diff --git a/Source/Core/AudioGraph/EffectsUnits/ReplayGain/ReplayGainUnitDelegate.swift b/Source/Core/AudioGraph/EffectsUnits/ReplayGain/ReplayGainUnitDelegate.swift index 65f10cc89..c0c525f6a 100644 --- a/Source/Core/AudioGraph/EffectsUnits/ReplayGain/ReplayGainUnitDelegate.swift +++ b/Source/Core/AudioGraph/EffectsUnits/ReplayGain/ReplayGainUnitDelegate.swift @@ -14,6 +14,8 @@ import Foundation class ReplayGainUnitDelegate: EffectsUnitDelegate, ReplayGainUnitDelegateProtocol { + static let cache: ConcurrentMap = ConcurrentMap() + var dataSource: ReplayGainDataSource { get {unit.dataSource} @@ -63,16 +65,51 @@ class ReplayGainUnitDelegate: EffectsUnitDelegate, ReplayGainUni var isScanning: Bool {_isScanning.value} private var _isScanning: AtomicBool = AtomicBool(value: false) - func initiateScan(forFile file: URL) { + func applyReplayGain(forTrack track: Track?) { - do { + guard let theTrack = track else { + + unit.replayGain = nil + return + } + + switch unit.dataSource { - let scanner = try ReplayGainScanner(file: file) + case .metadataOrAnalysis: + + if let replayGain = theTrack.replayGain { + + // Has metadata + unit.replayGain = replayGain + print("Found RG metadata: \(replayGain.trackGain ?? -100) for \(theTrack)") + + } else { + + // Analyze + analyze(file: theTrack.file) + print("No RG metadata for \(theTrack), analyzing ...") + } + + case .metadataOnly: + + print("Applying RG metadata: \(theTrack.replayGain?.trackGain ?? -100) for \(theTrack)") + unit.replayGain = theTrack.replayGain + + case .analysisOnly: + + print("Analyzing \(theTrack)") + analyze(file: theTrack.file) + } + } + + private func analyze(file: URL) { + + do { _isScanning.setTrue() Messenger.publish(.Effects.ReplayGainUnit.scanInitiated) - scanner.scan {[weak self] replayGain in + try replayGainScanner.scan(forFile: file) {[weak self] (replayGain: ReplayGain?) in guard let strongSelf = self else {return} diff --git a/Source/Core/AudioGraph/EffectsUnits/ReplayGain/ReplayGainUnitDelegateProtocol.swift b/Source/Core/AudioGraph/EffectsUnits/ReplayGain/ReplayGainUnitDelegateProtocol.swift index 44d5fa518..0075781c3 100644 --- a/Source/Core/AudioGraph/EffectsUnits/ReplayGain/ReplayGainUnitDelegateProtocol.swift +++ b/Source/Core/AudioGraph/EffectsUnits/ReplayGain/ReplayGainUnitDelegateProtocol.swift @@ -12,6 +12,8 @@ import Foundation protocol ReplayGainUnitDelegateProtocol: EffectsUnitDelegateProtocol { + func applyReplayGain(forTrack track: Track?) + var mode: ReplayGainMode {get set} // NOTE - Replay gain values will be set upon track change (values derived from metadata). @@ -22,8 +24,6 @@ protocol ReplayGainUnitDelegateProtocol: EffectsUnitDelegateProtocol { var preventClipping: Bool {get set} - func applyGain(_ replayGain: ReplayGain?) - var appliedGain: Float {get} var dataSource: ReplayGainDataSource {get set} @@ -34,7 +34,5 @@ protocol ReplayGainUnitDelegateProtocol: EffectsUnitDelegateProtocol { var effectiveGain: Float {get} - func initiateScan(forFile file: URL) - var isScanning: Bool {get} } diff --git a/Source/Core/EBUR128/EBUR128State.swift b/Source/Core/EBUR128/EBUR128State.swift index 6438e34d1..da598903d 100644 --- a/Source/Core/EBUR128/EBUR128State.swift +++ b/Source/Core/EBUR128/EBUR128State.swift @@ -25,7 +25,6 @@ class EBUR128State { let mode: EBUR128Mode static let targetLoudness: Double = -18 - static let maxPeak: Double = 1 init(channelCount: Int, sampleRate: Int, mode: EBUR128Mode) throws { @@ -141,7 +140,7 @@ enum EBUR128Mode { } } -struct EBUR128AnalysisResult { +struct EBUR128AnalysisResult: Codable { let loudness: Double let peak: Double diff --git a/Source/Core/Globals.swift b/Source/Core/Globals.swift index 606a7f138..628d607dc 100644 --- a/Source/Core/Globals.swift +++ b/Source/Core/Globals.swift @@ -109,6 +109,8 @@ let playbackDelegate: PlaybackDelegateProtocol = { var playbackInfoDelegate: PlaybackInfoDelegateProtocol {playbackDelegate} +let replayGainScanner = ReplayGainScanner(persistentState: appPersistentState.audioGraph?.replayGainAnalysisCache) + var historyDelegate: HistoryDelegateProtocol {playQueueDelegate} var favoritesDelegate: FavoritesDelegateProtocol {_favoritesDelegate} diff --git a/Source/Core/Persistence/Model/AudioGraph/AudioGraphPersistentState.swift b/Source/Core/Persistence/Model/AudioGraph/AudioGraphPersistentState.swift index d42292c7b..5bea66669 100644 --- a/Source/Core/Persistence/Model/AudioGraph/AudioGraphPersistentState.swift +++ b/Source/Core/Persistence/Model/AudioGraph/AudioGraphPersistentState.swift @@ -36,6 +36,8 @@ struct AudioGraphPersistentState: Codable { let soundProfiles: [SoundProfilePersistentState]? + let replayGainAnalysisCache: ReplayGainAnalysisCachePersistentState? + init(outputDevice: AudioDevicePersistentState?, volume: Float?, muted: Bool?, @@ -50,7 +52,8 @@ struct AudioGraphPersistentState: Codable { replayGainUnit: ReplayGainUnitPersistentState?, audioUnits: [AudioUnitPersistentState]?, audioUnitPresets: AudioUnitPresetsPersistentState?, - soundProfiles: [SoundProfilePersistentState]?) { + soundProfiles: [SoundProfilePersistentState]?, + replayGainAnalysisCache: ReplayGainAnalysisCachePersistentState?) { self.outputDevice = outputDevice self.volume = volume @@ -69,6 +72,7 @@ struct AudioGraphPersistentState: Codable { self.audioUnitPresets = audioUnitPresets self.soundProfiles = soundProfiles + self.replayGainAnalysisCache = replayGainAnalysisCache } init(legacyPersistentState: LegacyAudioGraphPersistentState?) { @@ -92,5 +96,6 @@ struct AudioGraphPersistentState: Codable { self.audioUnitPresets = AudioUnitPresetsPersistentState(legacyPersistentState: legacyPersistentState?.audioUnitPresets) self.soundProfiles = legacyPersistentState?.soundProfiles?.map {SoundProfilePersistentState(legacyPersistentState: $0)} + self.replayGainAnalysisCache = nil } } diff --git a/Source/Core/Persistence/Model/AudioGraph/ReplayGainUnitPersistentState.swift b/Source/Core/Persistence/Model/AudioGraph/ReplayGainUnitPersistentState.swift index 808c8a937..a8b27104e 100644 --- a/Source/Core/Persistence/Model/AudioGraph/ReplayGainUnitPersistentState.swift +++ b/Source/Core/Persistence/Model/AudioGraph/ReplayGainUnitPersistentState.swift @@ -59,3 +59,8 @@ struct ReplayGainPresetPersistentState: Codable { self.preventClipping = preset.preventClipping } } + +struct ReplayGainAnalysisCachePersistentState: Codable { + + let cache: [URL: EBUR128AnalysisResult]? +} diff --git a/Source/Core/Playback/Delegates/PlaybackChain/StartPlaybackChain/StartPlaybackAction.swift b/Source/Core/Playback/Delegates/PlaybackChain/StartPlaybackChain/StartPlaybackAction.swift index a9199a8fe..c1fa6d2a6 100644 --- a/Source/Core/Playback/Delegates/PlaybackChain/StartPlaybackChain/StartPlaybackAction.swift +++ b/Source/Core/Playback/Delegates/PlaybackChain/StartPlaybackChain/StartPlaybackAction.swift @@ -16,8 +16,6 @@ class StartPlaybackAction: PlaybackChainAction { private let player: PlayerProtocol - private lazy var messenger = Messenger(for: self) - init(_ player: PlayerProtocol) { self.player = player } @@ -34,14 +32,14 @@ class StartPlaybackAction: PlaybackChainAction { // Publish a pre-track-change notification for observers who need to perform actions before the track changes. // e.g. applying audio settings/effects. if context.currentTrack != newTrack { - messenger.publish(PreTrackPlaybackNotification(oldTrack: context.currentTrack, oldState: context.currentState, newTrack: newTrack)) + Messenger.publish(PreTrackPlaybackNotification(oldTrack: context.currentTrack, oldState: context.currentState, newTrack: newTrack)) } // Start playback player.play(newTrack, context.requestParams.startPosition, context.requestParams.endPosition) // Inform observers of the track change/transition. - messenger.publish(TrackTransitionNotification(beginTrack: context.currentTrack, beginState: context.currentState, + Messenger.publish(TrackTransitionNotification(beginTrack: context.currentTrack, beginState: context.currentState, endTrack: context.requestedTrack, endState: .playing)) chain.proceed(context) diff --git a/Source/Core/TrackIO/Model/ReplayGain.swift b/Source/Core/TrackIO/Model/ReplayGain.swift index 9ee11f5d7..e7242b108 100644 --- a/Source/Core/TrackIO/Model/ReplayGain.swift +++ b/Source/Core/TrackIO/Model/ReplayGain.swift @@ -35,8 +35,9 @@ struct ReplayGain { private func gainToPreventClipping(gain: Float, peak: Float, usingMaxPeakLevel maxPeakLevel: Float) -> Float { + let maxPeak = pow(10.0, maxPeakLevel / 20.0) let newPeak = pow(10.0, gain / 20) * peak - return newPeak > maxPeakLevel ? gain - (20 * log10(newPeak / maxPeakLevel)) : gain + return newPeak > maxPeak ? gain - (20 * log10(newPeak / maxPeak)) : gain } mutating func applyClippingPrevention(usingMaxPeakLevel maxPeakLevel: Float) { diff --git a/Source/Core/Utils/DataStructures/Concurrent/ConcurrentMap.swift b/Source/Core/Utils/DataStructures/Concurrent/ConcurrentMap.swift index 4969703e9..8460b286f 100644 --- a/Source/Core/Utils/DataStructures/Concurrent/ConcurrentMap.swift +++ b/Source/Core/Utils/DataStructures/Concurrent/ConcurrentMap.swift @@ -18,6 +18,10 @@ class ConcurrentMap { private(set) var map: [T: U] = [:] + var count: Int { + map.count + } + subscript(_ key: T) -> U? { get {