diff --git a/Aural.xcodeproj/project.pbxproj b/Aural.xcodeproj/project.pbxproj index cf44bff69..5062aaa7b 100644 --- a/Aural.xcodeproj/project.pbxproj +++ b/Aural.xcodeproj/project.pbxproj @@ -477,7 +477,8 @@ 3E0219D12C23497D00865AC2 /* shufen.regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 3E0219BD2C23497D00865AC2 /* shufen.regular.ttf */; }; 3E0219D22C23497D00865AC2 /* WalterTurncoat.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 3E0219BE2C23497D00865AC2 /* WalterTurncoat.ttf */; }; 3E0219EA2C23498700865AC2 /* appIcon.icns in Resources */ = {isa = PBXBuildFile; fileRef = 3E0219D32C23498700865AC2 /* appIcon.icns */; }; - 3E0328252C77BF7000A389B6 /* ReplayGainScannerOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E0328242C77BF7000A389B6 /* ReplayGainScannerOperation.swift */; }; + 3E0328252C77BF7000A389B6 /* ReplayGainTrackScannerOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E0328242C77BF7000A389B6 /* ReplayGainTrackScannerOperation.swift */; }; + 3E0328272C794D1200A389B6 /* ReplayGainAlbumScannerOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E0328262C794D1200A389B6 /* ReplayGainAlbumScannerOperation.swift */; }; 3E0397E82B83EB73004454DB /* AppDelegate+Init.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E0397E72B83EB73004454DB /* AppDelegate+Init.swift */; }; 3E0397EA2B83EC27004454DB /* AppDelegate+TearDown.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E0397E92B83EC27004454DB /* AppDelegate+TearDown.swift */; }; 3E045D06281322450069DEFE /* TrackInfoWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = 3E045D05281322450069DEFE /* TrackInfoWindow.xib */; }; @@ -1544,7 +1545,8 @@ 3E0219E42C23498700865AC2 /* copyright.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = copyright.txt; sourceTree = ""; }; 3E0219E52C23498700865AC2 /* xml-copyright.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = "xml-copyright.sh"; sourceTree = ""; }; 3E0219E72C23498700865AC2 /* unused.rb */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.ruby; path = unused.rb; sourceTree = ""; }; - 3E0328242C77BF7000A389B6 /* ReplayGainScannerOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplayGainScannerOperation.swift; sourceTree = ""; }; + 3E0328242C77BF7000A389B6 /* ReplayGainTrackScannerOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplayGainTrackScannerOperation.swift; sourceTree = ""; }; + 3E0328262C794D1200A389B6 /* ReplayGainAlbumScannerOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplayGainAlbumScannerOperation.swift; sourceTree = ""; }; 3E0397E72B83EB73004454DB /* AppDelegate+Init.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+Init.swift"; sourceTree = ""; }; 3E0397E92B83EC27004454DB /* AppDelegate+TearDown.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+TearDown.swift"; sourceTree = ""; }; 3E045D05281322450069DEFE /* TrackInfoWindow.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TrackInfoWindow.xib; sourceTree = ""; }; @@ -5376,7 +5378,8 @@ isa = PBXGroup; children = ( 3E772EC42C73F80C00DC3137 /* ReplayGainScanner.swift */, - 3E0328242C77BF7000A389B6 /* ReplayGainScannerOperation.swift */, + 3E0328242C77BF7000A389B6 /* ReplayGainTrackScannerOperation.swift */, + 3E0328262C794D1200A389B6 /* ReplayGainAlbumScannerOperation.swift */, 3EE882792C72A0DF00E270B8 /* AVFReplayGainScanner.swift */, 3EE8827A2C72A0DF00E270B8 /* FFmpegReplayGainScanner.swift */, 3E772EBC2C73F1B900DC3137 /* FFmpegReplayGainScanner+Scan.swift */, @@ -5387,8 +5390,8 @@ 3EE8827F2C72A11300E270B8 /* EBUR128 */ = { isa = PBXGroup; children = ( - 3EE8827D2C72A11300E270B8 /* EBUR128Errors.swift */, 3EE8827E2C72A11300E270B8 /* EBUR128State.swift */, + 3EE8827D2C72A11300E270B8 /* EBUR128Errors.swift */, ); path = EBUR128; sourceTree = ""; @@ -5864,7 +5867,7 @@ 3E6C125525CEBE0600BF0D07 /* ColorSchemePreviewView.swift in Sources */, 3E6C127725CEBE1800BF0D07 /* ColorSchemesManager.swift in Sources */, 3EFFEB8027D9CB25006A333B /* ColorSchemesManager+Observer.swift in Sources */, - 3E0328252C77BF7000A389B6 /* ReplayGainScannerOperation.swift in Sources */, + 3E0328252C77BF7000A389B6 /* ReplayGainTrackScannerOperation.swift in Sources */, 3E6C126A25CEBE0600BF0D07 /* ColorSchemesManagerViewController.swift in Sources */, 3E0217FE2C23490E00865AC2 /* EQUnitProtocol.swift in Sources */, 3EB3A61926A763870060487C /* ColorSchemesViewProtocol.swift in Sources */, @@ -6409,6 +6412,7 @@ 3E0219072C23490E00865AC2 /* LegacyGesturesControlsPreferences.swift in Sources */, 3E4A90FE27F79AFA003A6C80 /* WindowID.swift in Sources */, 3EEC08B92BDC5F9A008DBF8E /* SearchViewController+Theming.swift in Sources */, + 3E0328272C794D1200A389B6 /* ReplayGainAlbumScannerOperation.swift in Sources */, 3EB3A60B26A7425C0060487C /* SingletonWindowController.swift in Sources */, 3EEBC6EE27C16CF700880DFD /* EffectsUnitStateObserverRegistry.swift in Sources */, 3E0219342C23490E00865AC2 /* Chapter.swift in Sources */, diff --git a/Aural.xcodeproj/project.xcworkspace/xcuserdata/kven.xcuserdatad/UserInterfaceState.xcuserstate b/Aural.xcodeproj/project.xcworkspace/xcuserdata/kven.xcuserdatad/UserInterfaceState.xcuserstate index 453322ec9..43a917237 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/EffectsUnits/ReplayGain/LoudnessScan/AVFReplayGainScanner.swift b/Source/Core/AudioGraph/EffectsUnits/ReplayGain/LoudnessScan/AVFReplayGainScanner.swift index 59f9930e1..347a0a583 100644 --- a/Source/Core/AudioGraph/EffectsUnits/ReplayGain/LoudnessScan/AVFReplayGainScanner.swift +++ b/Source/Core/AudioGraph/EffectsUnits/ReplayGain/LoudnessScan/AVFReplayGainScanner.swift @@ -33,7 +33,7 @@ class AVFReplayGainScanner: EBUR128LoudnessScannerProtocol { private static let maxConsecutiveIOErrors: Int = 3 private static let maxTotalIOErrors: Int = 10 - init(file: URL) throws { + required init(file: URL) throws { self.file = file @@ -54,93 +54,89 @@ class AVFReplayGainScanner: EBUR128LoudnessScannerProtocol { channelLayout: channelLayout) } - func scan(_ completionHandler: @escaping (EBUR128AnalysisResult?) -> Void) { + func scan() throws -> EBUR128TrackAnalysisResult { - DispatchQueue.global(qos: .userInitiated).async { + var samplesRead: AVAudioFramePosition = 0 + + guard let readBuffer = AVAudioPCMBuffer(pcmFormat: audioFormat, frameCapacity: Self.chunkSize), + let analyzeBuffer = AVAudioPCMBuffer(pcmFormat: analysisFormat, frameCapacity: Self.chunkSize) else { - do { - completionHandler(try self.doScan()) - - } catch let err as EBUR128Error { - print("Error: \(err.description)") - - } catch { - print("Error: \(error)") - } + throw AVFoundationError("Unable to create AVAudioPCMBuffer with format: \(audioFormat) and capacity: \(Self.chunkSize)") } - } - - private func doScan() throws -> EBUR128AnalysisResult? { - do { - - var samplesRead: AVAudioFramePosition = 0 - - guard let readBuffer = AVAudioPCMBuffer(pcmFormat: audioFormat, frameCapacity: Self.chunkSize), - let analyzeBuffer = AVAudioPCMBuffer(pcmFormat: analysisFormat, frameCapacity: Self.chunkSize) else {return nil} + guard let converter: AVAudioConverter = .init(from: audioFormat, + to: analysisFormat) else { - guard let converter: AVAudioConverter = .init(from: audioFormat, - to: analysisFormat) else {return nil} - - var eof: Bool = false - var sampleCountFromLastRead: AVAudioFramePosition = 0 - var consecutiveIOErrors: Int = 0 - var totalIOErrors: Int = 0 + throw AVFoundationError("Unable to create AVAudioConverter with source format: \(audioFormat) and target format: \(analysisFormat)") + } + + var eof: Bool = false + var sampleCountFromLastRead: AVAudioFramePosition = 0 + var consecutiveIOErrors: Int = 0 + var totalIOErrors: Int = 0 + var mostRecentError: Error? = nil + + while (!isCancelled) && (!eof) { - while (!isCancelled) && (!eof) { + do { - do { - - try audioFile.read(into: readBuffer) - sampleCountFromLastRead = AVAudioFramePosition(readBuffer.frameLength) - samplesRead += sampleCountFromLastRead - - try converter.convert(to: analyzeBuffer, from: readBuffer) - - guard let floatBuffer = analyzeBuffer.floatChannelData else {return nil} - - try ebur128.addFramesAsFloat(framesPointer: floatBuffer[0], frameCount: Int(analyzeBuffer.frameLength)) - - // Reset the error counter if the read after a failed iteration succeeds. - if consecutiveIOErrors > 0 { - consecutiveIOErrors = 0 - } - - } catch { + try audioFile.read(into: readBuffer) + sampleCountFromLastRead = AVAudioFramePosition(readBuffer.frameLength) + samplesRead += sampleCountFromLastRead + + try converter.convert(to: analyzeBuffer, from: readBuffer) + + guard let floatBuffer = analyzeBuffer.floatChannelData else { + throw AVFoundationError("Unable to get floatChannelData property of AVAudioPCMBuffer") + } + + try ebur128.addFramesAsFloat(framesPointer: floatBuffer[0], frameCount: Int(analyzeBuffer.frameLength)) + + // Reset the error counter if the read after a failed iteration succeeds. + if consecutiveIOErrors > 0 { + consecutiveIOErrors = 0 + } + + } catch { + + let description = (error as? EBUR128Error)?.description ?? error.localizedDescription + NSLog("Waveform Decoder IO Error: \(description)") + + mostRecentError = error + consecutiveIOErrors.increment() + totalIOErrors.increment() + + if consecutiveIOErrors >= Self.maxConsecutiveIOErrors { - let description = (error as? EBUR128Error)?.description ?? error.localizedDescription - NSLog("Waveform Decoder IO Error: \(description)") + NSLog("Encountered too many consecutive IO errors. Terminating scan loop.") + break - consecutiveIOErrors.increment() - totalIOErrors.increment() + } else if totalIOErrors > Self.maxTotalIOErrors { - if consecutiveIOErrors >= Self.maxConsecutiveIOErrors { - - NSLog("Encountered too many consecutive IO errors. Terminating scan loop.") - break - - } else if totalIOErrors > Self.maxTotalIOErrors { - - NSLog("Encountered too many total IO errors. Terminating scan loop.") - break - } + NSLog("Encountered too many total IO errors. Terminating scan loop.") + break } - - eof = (audioFile.framePosition >= totalSamples) || - (samplesRead >= totalSamples) || - (sampleCountFromLastRead == 0) } - - return isCancelled || (!eof) ? nil : try ebur128.analyze() - - } catch { - print("Error: \(error.localizedDescription)") - return nil + eof = (audioFile.framePosition >= totalSamples) || + (samplesRead >= totalSamples) || + (sampleCountFromLastRead == 0) } + + if isCancelled { + throw EBURAnalysisInterruptedError(rootCause: mostRecentError, message: "Operation was cancelled.") + } else if consecutiveIOErrors >= 3 { + throw EBURAnalysisInterruptedError(rootCause: mostRecentError, message: "Too many consecutive errors encountered.") + } else if !eof { + throw EBURAnalysisInterruptedError(rootCause: mostRecentError, message: "Did not reach EOF.") + } + + return try ebur128.analyze() } func cancel() { isCancelled = true } } + +class AVFoundationError: DisplayableError {} diff --git a/Source/Core/AudioGraph/EffectsUnits/ReplayGain/LoudnessScan/FFmpegReplayGainScanner+Scan.swift b/Source/Core/AudioGraph/EffectsUnits/ReplayGain/LoudnessScan/FFmpegReplayGainScanner+Scan.swift index dd6705b32..65ef4fc60 100644 --- a/Source/Core/AudioGraph/EffectsUnits/ReplayGain/LoudnessScan/FFmpegReplayGainScanner+Scan.swift +++ b/Source/Core/AudioGraph/EffectsUnits/ReplayGain/LoudnessScan/FFmpegReplayGainScanner+Scan.swift @@ -14,7 +14,7 @@ fileprivate typealias EBUR128FramesAddFunction = (UnsafeMutablePointer?, extension FFmpegReplayGainScanner { - func scanAsInt16() throws -> EBUR128AnalysisResult? { + func scanAsInt16() throws -> EBUR128TrackAnalysisResult { try doScan {pointer, frame in @@ -34,7 +34,7 @@ extension FFmpegReplayGainScanner { } } - func scanAsInt32() throws -> EBUR128AnalysisResult? { + func scanAsInt32() throws -> EBUR128TrackAnalysisResult { try doScan {pointer, frame in @@ -54,7 +54,7 @@ extension FFmpegReplayGainScanner { } } - func scanAsFloat() throws -> EBUR128AnalysisResult? { + func scanAsFloat() throws -> EBUR128TrackAnalysisResult { try doScan {pointer, frame in @@ -74,7 +74,7 @@ extension FFmpegReplayGainScanner { } } - func scanAsDouble() throws -> EBUR128AnalysisResult? { + func scanAsDouble() throws -> EBUR128TrackAnalysisResult { try doScan {pointer, frame in @@ -94,60 +94,62 @@ extension FFmpegReplayGainScanner { } } - fileprivate func doScan(addFramesFunction: EBUR128FramesAddFunction) throws -> EBUR128AnalysisResult? { + fileprivate func doScan(addFramesFunction: EBUR128FramesAddFunction) throws -> EBUR128TrackAnalysisResult { defer { self.cleanUpAfterScan() } - do { - - var curSize: Int = 0 - let sizeOfAFrame = codec.sampleFormat.size * channelCount + var mostRecentError: Error? = nil + var curSize: Int = 0 + let sizeOfAFrame = codec.sampleFormat.size * channelCount + + while !isCancelled, !eof, consecutiveErrors < 3 { - while !isCancelled, !eof, consecutiveErrors < 3 { + do { - do { + guard let pkt = try ctx.readPacket(from: stream) else { - guard let pkt = try ctx.readPacket(from: stream) else { - - consecutiveErrors.increment() - continue - } + consecutiveErrors.increment() + continue + } + + let frames = try codec.decode(packet: pkt) + + for frame in frames.frames { - let frames = try codec.decode(packet: pkt) + // Only 1 buffer since interleaved. Capacity = sampleCount * number of bytes in Int16 * channelCount + let newSize = frame.intSampleCount * sizeOfAFrame - for frame in frames.frames { + if newSize > curSize { - // Only 1 buffer since interleaved. Capacity = sampleCount * number of bytes in Int16 * channelCount - let newSize = frame.intSampleCount * sizeOfAFrame - - if newSize > curSize { - - outputData?[0] = .allocate(capacity: newSize) - curSize = newSize - } - - swr?.convertFrame(frame, andStoreIn: outputData) - addFramesFunction(outputData?[0] ?? frame.dataPointers[0], frame) + outputData?[0] = .allocate(capacity: newSize) + curSize = newSize } - } catch let err as CodedError { - - eof = err.isEOF - - if !err.isEOF { - - consecutiveErrors.increment() - print("Error: \(err.code.errorDescription)") - } + swr?.convertFrame(frame, andStoreIn: outputData) + addFramesFunction(outputData?[0] ?? frame.dataPointers[0], frame) + } + + } catch let err as CodedError { + + eof = err.isEOF + mostRecentError = err + + if !err.isEOF { + consecutiveErrors.increment() } } - - } catch { - print("Error: \(error.localizedDescription)") } - return isCancelled || (consecutiveErrors >= 3) || (!eof) ? nil : try ebur128.analyze() + if isCancelled { + throw EBURAnalysisInterruptedError(rootCause: mostRecentError, message: "Operation was cancelled.") + } else if consecutiveErrors >= 3 { + throw EBURAnalysisInterruptedError(rootCause: mostRecentError, message: "Too many consecutive errors encountered.") + } else if !eof { + throw EBURAnalysisInterruptedError(rootCause: mostRecentError, message: "Did not reach EOF.") + } + + return try ebur128.analyze() } } diff --git a/Source/Core/AudioGraph/EffectsUnits/ReplayGain/LoudnessScan/FFmpegReplayGainScanner.swift b/Source/Core/AudioGraph/EffectsUnits/ReplayGain/LoudnessScan/FFmpegReplayGainScanner.swift index beed42de4..3ba74d0b6 100644 --- a/Source/Core/AudioGraph/EffectsUnits/ReplayGain/LoudnessScan/FFmpegReplayGainScanner.swift +++ b/Source/Core/AudioGraph/EffectsUnits/ReplayGain/LoudnessScan/FFmpegReplayGainScanner.swift @@ -33,7 +33,7 @@ class FFmpegReplayGainScanner: EBUR128LoudnessScannerProtocol { var consecutiveErrors: Int = 0 var eof: Bool = false - init(file: URL) throws { + required init(file: URL) throws { self.file = file @@ -67,37 +67,24 @@ class FFmpegReplayGainScanner: EBUR128LoudnessScannerProtocol { } } - func scan(_ completionHandler: @escaping (EBUR128AnalysisResult?) -> Void) { + func scan() throws -> EBUR128TrackAnalysisResult { - DispatchQueue.global(qos: .userInitiated).async { + switch self.targetFormat { - do { - - var result: EBUR128AnalysisResult? = nil - - switch self.targetFormat { - - case AV_SAMPLE_FMT_S16: - result = try self.scanAsInt16() - - case AV_SAMPLE_FMT_S32: - result = try self.scanAsInt32() - - case AV_SAMPLE_FMT_FLT: - result = try self.scanAsFloat() - - case AV_SAMPLE_FMT_DBL: - result = try self.scanAsDouble() - - default: - break - } - - completionHandler(result) - - } catch { - print((error as? EBUR128Error)?.description ?? error.localizedDescription) - } + case AV_SAMPLE_FMT_S16: + return try self.scanAsInt16() + + case AV_SAMPLE_FMT_S32: + return try self.scanAsInt32() + + case AV_SAMPLE_FMT_FLT: + return try self.scanAsFloat() + + case AV_SAMPLE_FMT_DBL: + return try self.scanAsDouble() + + default: + return try self.scanAsInt16() } } diff --git a/Source/Core/AudioGraph/EffectsUnits/ReplayGain/LoudnessScan/ReplayGainAlbumScannerOperation.swift b/Source/Core/AudioGraph/EffectsUnits/ReplayGain/LoudnessScan/ReplayGainAlbumScannerOperation.swift new file mode 100644 index 000000000..7abe4b3cd --- /dev/null +++ b/Source/Core/AudioGraph/EffectsUnits/ReplayGain/LoudnessScan/ReplayGainAlbumScannerOperation.swift @@ -0,0 +1,95 @@ +// +// ReplayGainAlbumScannerOperation.swift +// Aural +// +// 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. +// + +import Foundation + +class ReplayGainAlbumScannerOperation: Operation { + + let files: [URL] + + private var scanners: [EBUR128LoudnessScannerProtocol] = [] + private let completionHandler: (ReplayGainAlbumScannerOperation, EBUR128AlbumAnalysisResult?) -> Void + + // ------------------------------------------------------------------------------------------------------------------- + + // MARK: - NSOperation Overrides + + override var isAsynchronous: Bool {true} + + /// Backing value for ``isExecuting``. + private var _isExecuting = false + override var isExecuting: Bool {_isExecuting} + + /// Backing value for ``isFinished``. + private var _isFinished = false + override var isFinished: Bool {_isFinished} + + init(files: [URL], completionHandler: @escaping (ReplayGainAlbumScannerOperation, EBUR128AlbumAnalysisResult?) -> Void) throws { + + self.files = files + self.completionHandler = completionHandler + +// self.scanner = file.isNativelySupported ? +// try AVFReplayGainScanner(file: file) : +// try FFmpegReplayGainScanner(file: file) + + super.init() + } + + override func start() { + + // Do nothing if any of these flags is set. + guard !isExecuting, !isFinished, !isCancelled else {return} + + // Update state for KVO. + willChangeValue(forKey: "isExecuting") + _isExecuting = true + didChangeValue(forKey: "isExecuting") + + DispatchQueue.global(qos: .userInitiated).async { + + var result: EBUR128AlbumAnalysisResult? = nil + +// do { +// result = try self.scanner.scan() +// } catch { +// NSLog("EBUR128 analysis of file '\(self.file.path)' failed. Error: \((error as? EBUR128Error)?.description ?? error.localizedDescription)") +// } + + self.completionHandler(self, result) + self.finish() + } + } + + private func finish() { + + // Do nothing if any of these flags is set. + guard !isFinished, !isCancelled else {return} + + // Update state for KVO. + // NOTE - ``completionHandler`` will be called automatically + // by ``NSOperation`` after these values change. + + willChangeValue(forKey: "isExecuting") + willChangeValue(forKey: "isFinished") + _isExecuting = false + _isFinished = true + didChangeValue(forKey: "isExecuting") + didChangeValue(forKey: "isFinished") + } + + override func cancel() { + + super.cancel() +// scanner.cancel() + +// print("\nScan op cancelled for file: \(file.lastPathComponent)") + } +} diff --git a/Source/Core/AudioGraph/EffectsUnits/ReplayGain/LoudnessScan/ReplayGainScanner.swift b/Source/Core/AudioGraph/EffectsUnits/ReplayGain/LoudnessScan/ReplayGainScanner.swift index c944120d9..ab57329d7 100644 --- a/Source/Core/AudioGraph/EffectsUnits/ReplayGain/LoudnessScan/ReplayGainScanner.swift +++ b/Source/Core/AudioGraph/EffectsUnits/ReplayGain/LoudnessScan/ReplayGainScanner.swift @@ -14,7 +14,9 @@ protocol EBUR128LoudnessScannerProtocol { var file: URL {get} - func scan(_ completionHandler: @escaping (EBUR128AnalysisResult?) -> Void) + init(file: URL) throws + + func scan() throws -> EBUR128TrackAnalysisResult func cancel() @@ -23,9 +25,9 @@ protocol EBUR128LoudnessScannerProtocol { class ReplayGainScanner { - let cache: ConcurrentMap = ConcurrentMap() + let cache: ConcurrentMap = ConcurrentMap() - private var scanOp: ReplayGainScannerOperation? = nil + private var scanOp: ReplayGainTrackScannerOperation? = nil init(persistentState: ReplayGainAnalysisCachePersistentState?) { @@ -52,7 +54,7 @@ class ReplayGainScanner { // Cache miss, initiate a scan - scanOp = try ReplayGainScannerOperation(file: file) {[weak self] finishedScanOp, ebur128Result in + scanOp = try ReplayGainTrackScannerOperation(file: file) {[weak self] finishedScanOp, ebur128Result in // A previously scheduled scan op may finish just before being cancelled. This check // will prevent rogue completion handler execution. diff --git a/Source/Core/AudioGraph/EffectsUnits/ReplayGain/LoudnessScan/ReplayGainScannerOperation.swift b/Source/Core/AudioGraph/EffectsUnits/ReplayGain/LoudnessScan/ReplayGainTrackScannerOperation.swift similarity index 70% rename from Source/Core/AudioGraph/EffectsUnits/ReplayGain/LoudnessScan/ReplayGainScannerOperation.swift rename to Source/Core/AudioGraph/EffectsUnits/ReplayGain/LoudnessScan/ReplayGainTrackScannerOperation.swift index 025063d1b..65d92b4bf 100644 --- a/Source/Core/AudioGraph/EffectsUnits/ReplayGain/LoudnessScan/ReplayGainScannerOperation.swift +++ b/Source/Core/AudioGraph/EffectsUnits/ReplayGain/LoudnessScan/ReplayGainTrackScannerOperation.swift @@ -10,12 +10,12 @@ import Foundation -class ReplayGainScannerOperation: Operation { +class ReplayGainTrackScannerOperation: Operation { let file: URL - private let scanner: EBUR128LoudnessScannerProtocol - private let completionHandler: (ReplayGainScannerOperation, EBUR128AnalysisResult?) -> Void + private var scanner: EBUR128LoudnessScannerProtocol! + private let completionHandler: (ReplayGainTrackScannerOperation, EBUR128TrackAnalysisResult?) -> Void // ------------------------------------------------------------------------------------------------------------------- @@ -31,15 +31,11 @@ class ReplayGainScannerOperation: Operation { private var _isFinished = false override var isFinished: Bool {_isFinished} - init(file: URL, completionHandler: @escaping (ReplayGainScannerOperation, EBUR128AnalysisResult?) -> Void) throws { + init(file: URL, completionHandler: @escaping (ReplayGainTrackScannerOperation, EBUR128TrackAnalysisResult?) -> Void) { self.file = file self.completionHandler = completionHandler - self.scanner = file.isNativelySupported ? - try AVFReplayGainScanner(file: file) : - try FFmpegReplayGainScanner(file: file) - super.init() } @@ -55,13 +51,21 @@ class ReplayGainScannerOperation: Operation { DispatchQueue.global(qos: .userInitiated).async { - self.scanner.scan {[weak self] ebur128Result in + var result: EBUR128TrackAnalysisResult? = nil + + do { + + self.scanner = self.file.isNativelySupported ? + try AVFReplayGainScanner(file: self.file) : + try FFmpegReplayGainScanner(file: self.file) - if let strongSelf = self { - strongSelf.completionHandler(strongSelf, ebur128Result) - } + result = try self.scanner.scan() + + } catch { + NSLog("EBUR128 analysis of file '\(self.file.path)' failed. Error: \((error as? EBUR128Error)?.description ?? error.localizedDescription)") } - + + self.completionHandler(self, result) self.finish() } } diff --git a/Source/Core/AudioGraph/EffectsUnits/ReplayGain/ReplayGainUnitDelegate.swift b/Source/Core/AudioGraph/EffectsUnits/ReplayGain/ReplayGainUnitDelegate.swift index ab6292d70..6a07ff26b 100644 --- a/Source/Core/AudioGraph/EffectsUnits/ReplayGain/ReplayGainUnitDelegate.swift +++ b/Source/Core/AudioGraph/EffectsUnits/ReplayGain/ReplayGainUnitDelegate.swift @@ -12,7 +12,7 @@ import Foundation class ReplayGainUnitDelegate: EffectsUnitDelegate, ReplayGainUnitDelegateProtocol { - static let cache: ConcurrentMap = ConcurrentMap() + static let cache: ConcurrentMap = ConcurrentMap() var dataSource: ReplayGainDataSource { diff --git a/Source/Core/EBUR128/EBUR128Errors.swift b/Source/Core/EBUR128/EBUR128Errors.swift index 91b6062ef..3c0642ffe 100644 --- a/Source/Core/EBUR128/EBUR128Errors.swift +++ b/Source/Core/EBUR128/EBUR128Errors.swift @@ -73,3 +73,19 @@ class EBURAnalysisError: EBUR128Error { super.init(description: "EBUR128 failed to analyze frames. Result code: \(resultCode)") } } + +class EBURAnalysisInterruptedError: Error, CustomStringConvertible { + + let rootCause: Error? + let message: String? + + var description: String { + message ?? rootCause?.localizedDescription ?? "Unknown error" + } + + init(rootCause: Error?, message: String) { + + self.rootCause = rootCause + self.message = message + } +} diff --git a/Source/Core/EBUR128/EBUR128State.swift b/Source/Core/EBUR128/EBUR128State.swift index da598903d..df4611362 100644 --- a/Source/Core/EBUR128/EBUR128State.swift +++ b/Source/Core/EBUR128/EBUR128State.swift @@ -25,6 +25,7 @@ class EBUR128State { let mode: EBUR128Mode static let targetLoudness: Double = -18 + static let assumedPeak: Double = 1 init(channelCount: Int, sampleRate: Int, mode: EBUR128Mode) throws { @@ -75,16 +76,16 @@ class EBUR128State { } } - func analyze() throws -> EBUR128AnalysisResult { + func analyze() throws -> EBUR128TrackAnalysisResult { let loudness = try computeLoudness() let peak = try computePeak() let replayGain = Self.targetLoudness - loudness - return EBUR128AnalysisResult(loudness: loudness, peak: peak, replayGain: replayGain) + return EBUR128TrackAnalysisResult(loudness: loudness, peak: peak, replayGain: replayGain) } - func computeLoudness() throws -> Double { + private func computeLoudness() throws -> Double { var loudness: Double = 0 let result: EBUR128ResultCode = ebur128_loudness_global(self.pointer, &loudness) @@ -96,13 +97,11 @@ class EBUR128State { return loudness } - func computePeak() throws -> Double { + private func computePeak() throws -> Double { var peaks: [Double] = [] var result: EBUR128ResultCode = 0 - var peak: Double = 0 - let analysisFunction: PeakAnalysisFunction = self.mode == .samplePeak ? ebur128_sample_peak : ebur128_true_peak for channel in 0.. EBUR128AlbumAnalysisResult { + + var pointers: [UnsafeMutablePointer?] = eburs.map {$0.pointer} + var albumLoudness: Double = 0 + + ebur128_loudness_global_multiple(&pointers, eburs.count, &albumLoudness) + let albumPeak = trackResults.map {$0.peak}.max() ?? Self.assumedPeak + + return EBUR128AlbumAnalysisResult(albumLoudness: albumLoudness, + albumPeak: albumPeak, + albumReplayGain: Self.targetLoudness - albumLoudness, + trackResults: trackResults) + } + deinit { var mutablePointer: UnsafeMutablePointer? = self.pointer @@ -140,9 +154,18 @@ enum EBUR128Mode { } } -struct EBUR128AnalysisResult: Codable { +struct EBUR128TrackAnalysisResult: Codable { let loudness: Double let peak: Double let replayGain: Double } + +struct EBUR128AlbumAnalysisResult: Codable { + + let albumLoudness: Double + let albumPeak: Double + let albumReplayGain: Double + + let trackResults: [EBUR128TrackAnalysisResult] +} diff --git a/Source/Core/Persistence/Model/AudioGraph/ReplayGainUnitPersistentState.swift b/Source/Core/Persistence/Model/AudioGraph/ReplayGainUnitPersistentState.swift index a8b27104e..e26c22458 100644 --- a/Source/Core/Persistence/Model/AudioGraph/ReplayGainUnitPersistentState.swift +++ b/Source/Core/Persistence/Model/AudioGraph/ReplayGainUnitPersistentState.swift @@ -62,5 +62,5 @@ struct ReplayGainPresetPersistentState: Codable { struct ReplayGainAnalysisCachePersistentState: Codable { - let cache: [URL: EBUR128AnalysisResult]? + let cache: [URL: EBUR128TrackAnalysisResult]? } diff --git a/Source/Core/TrackIO/Model/ReplayGain.swift b/Source/Core/TrackIO/Model/ReplayGain.swift index 48e76495a..0cf92b6be 100644 --- a/Source/Core/TrackIO/Model/ReplayGain.swift +++ b/Source/Core/TrackIO/Model/ReplayGain.swift @@ -65,7 +65,7 @@ struct ReplayGain: Codable { } } - init(ebur128AnalysisResult: EBUR128AnalysisResult) { + init(ebur128AnalysisResult: EBUR128TrackAnalysisResult) { self.trackGain = Float(ebur128AnalysisResult.replayGain) self.trackPeak = Float(ebur128AnalysisResult.peak) diff --git a/Source/UI/Effects/ReplayGain/ReplayGainUnitViewController.swift b/Source/UI/Effects/ReplayGain/ReplayGainUnitViewController.swift index 6b2b674f1..4dd34d15a 100644 --- a/Source/UI/Effects/ReplayGain/ReplayGainUnitViewController.swift +++ b/Source/UI/Effects/ReplayGain/ReplayGainUnitViewController.swift @@ -119,7 +119,7 @@ class ReplayGainUnitViewController: EffectsUnitViewController { } private func scanInitiated() { - lblGain.stringValue = "Analyzing file loudness ..." + lblGain.stringValue = "Analyzing track loudness ..." } override func fontSchemeChanged() { diff --git a/Source/UI/PlayQueue/PlayQueueViewController+ContextMenuController.swift b/Source/UI/PlayQueue/PlayQueueViewController+ContextMenuController.swift index f7ff708f8..a5dd9c445 100644 --- a/Source/UI/PlayQueue/PlayQueueViewController+ContextMenuController.swift +++ b/Source/UI/PlayQueue/PlayQueueViewController+ContextMenuController.swift @@ -29,6 +29,9 @@ extension PlayQueueViewController: NSMenuDelegate { func menuNeedsUpdate(_ menu: NSMenu) { + let selectedRows = self.selectedRows + let selectedRowCount = selectedRows.count + let atLeastOneRowSelected = selectedRowCount >= 1 let oneRowSelected = selectedRowCount == 1 let notInGaplessMode = !playbackDelegate.isInGaplessPlaybackMode diff --git a/Source/UI/Waveform/WaveformViewController.swift b/Source/UI/Waveform/WaveformViewController.swift index 3da61c456..a2ce2d5cc 100644 --- a/Source/UI/Waveform/WaveformViewController.swift +++ b/Source/UI/Waveform/WaveformViewController.swift @@ -125,6 +125,10 @@ class WaveformViewController: NSViewController { if let window = view.window, window.isVisible { updateForTrack(notification.endTrack) } + + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + print("renderOp now: \(self.waveformView.renderOp)") + } } private func playbackLoopChanged() {