Skip to content

Commit

Permalink
#12 Fixed clipping prevention, implemented RG analysis cache
Browse files Browse the repository at this point in the history
  • Loading branch information
kartik-venugopal committed Aug 21, 2024
1 parent f6cd831 commit ba8c079
Show file tree
Hide file tree
Showing 13 changed files with 105 additions and 34 deletions.
Binary file not shown.
3 changes: 2 additions & 1 deletion Source/Core/AudioGraph/AudioGraph.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)

}
}
Expand Down
10 changes: 1 addition & 9 deletions Source/Core/AudioGraph/AudioGraphDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,26 +19,55 @@ protocol EBUR128LoudnessScannerProtocol {

class ReplayGainScanner {

let file: URL
let scanner: EBUR128LoudnessScannerProtocol
let cache: ConcurrentMap<URL, EBUR128AnalysisResult> = 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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import Foundation

class ReplayGainUnitDelegate: EffectsUnitDelegate<ReplayGainUnit>, ReplayGainUnitDelegateProtocol {

static let cache: ConcurrentMap<URL, EBUR128AnalysisResult> = ConcurrentMap()

var dataSource: ReplayGainDataSource {

get {unit.dataSource}
Expand Down Expand Up @@ -63,16 +65,51 @@ class ReplayGainUnitDelegate: EffectsUnitDelegate<ReplayGainUnit>, 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}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -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}
Expand All @@ -34,7 +34,5 @@ protocol ReplayGainUnitDelegateProtocol: EffectsUnitDelegateProtocol {

var effectiveGain: Float {get}

func initiateScan(forFile file: URL)

var isScanning: Bool {get}
}
3 changes: 1 addition & 2 deletions Source/Core/EBUR128/EBUR128State.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -141,7 +140,7 @@ enum EBUR128Mode {
}
}

struct EBUR128AnalysisResult {
struct EBUR128AnalysisResult: Codable {

let loudness: Double
let peak: Double
Expand Down
2 changes: 2 additions & 0 deletions Source/Core/Globals.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ struct AudioGraphPersistentState: Codable {

let soundProfiles: [SoundProfilePersistentState]?

let replayGainAnalysisCache: ReplayGainAnalysisCachePersistentState?

init(outputDevice: AudioDevicePersistentState?,
volume: Float?,
muted: Bool?,
Expand All @@ -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
Expand All @@ -69,6 +72,7 @@ struct AudioGraphPersistentState: Codable {

self.audioUnitPresets = audioUnitPresets
self.soundProfiles = soundProfiles
self.replayGainAnalysisCache = replayGainAnalysisCache
}

init(legacyPersistentState: LegacyAudioGraphPersistentState?) {
Expand All @@ -92,5 +96,6 @@ struct AudioGraphPersistentState: Codable {
self.audioUnitPresets = AudioUnitPresetsPersistentState(legacyPersistentState: legacyPersistentState?.audioUnitPresets)

self.soundProfiles = legacyPersistentState?.soundProfiles?.map {SoundProfilePersistentState(legacyPersistentState: $0)}
self.replayGainAnalysisCache = nil
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,8 @@ struct ReplayGainPresetPersistentState: Codable {
self.preventClipping = preset.preventClipping
}
}

struct ReplayGainAnalysisCachePersistentState: Codable {

let cache: [URL: EBUR128AnalysisResult]?
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ class StartPlaybackAction: PlaybackChainAction {

private let player: PlayerProtocol

private lazy var messenger = Messenger(for: self)

init(_ player: PlayerProtocol) {
self.player = player
}
Expand All @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion Source/Core/TrackIO/Model/ReplayGain.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ class ConcurrentMap<T: Hashable, U: Any> {

private(set) var map: [T: U] = [:]

var count: Int {
map.count
}

subscript(_ key: T) -> U? {

get {
Expand Down

0 comments on commit ba8c079

Please sign in to comment.