-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathAudioNormalizationCalculator.swift
More file actions
144 lines (131 loc) · 4.46 KB
/
Copy pathAudioNormalizationCalculator.swift
File metadata and controls
144 lines (131 loc) · 4.46 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
//
// AudioNormalizationCalculator.swift
// PlayolaPlayer
//
// Created by Brian D Keane on 2/7/25.
//
import AVFoundation
struct AudioNormalizationCalculator {
enum AudioError: Error {
case fileConversion
case bufferConversion
}
let file: AVAudioFile
var amplitude: Float?
public func playerVolume(_ adjustedVolume: Float) -> Float {
return (amplitude ?? 1.0) * adjustedVolume
}
/// Creates an AudioNormalizationCalculator with amplitude calculated on a background thread
static func create(_ file: AVAudioFile) async -> AudioNormalizationCalculator {
return await withCheckedContinuation { continuation in
Task.detached(priority: .userInitiated) {
var calculator = AudioNormalizationCalculator(file: file, calculateNow: false)
do {
let samples = try calculator.getSamples()
calculator.amplitude = calculator.getAmplitude(samples)
} catch {
// Report error on main thread
await PlayolaErrorReporter.shared.reportError(
error,
context:
"Failed to get audio samples for normalization | File: \(file.url.lastPathComponent) | "
+ "Format: \(file.processingFormat.description) | Length: \(file.length)",
level: .warning)
}
continuation.resume(returning: calculator)
}
}
}
/// Synchronous initializer for backwards compatibility (e.g., tests)
init(_ file: AVAudioFile) {
self.file = file
do {
let samples = try getSamples()
self.amplitude = getAmplitude(samples)
} catch {
// Replace print with proper error reporting
Task {
await PlayolaErrorReporter.shared.reportError(
error,
context:
"Failed to get audio samples for normalization | File: \(file.url.lastPathComponent) | "
+ "Format: \(file.processingFormat.description) | Length: \(file.length)",
level: .warning)
}
}
}
/// Private initializer used by async factory
private init(file: AVAudioFile, calculateNow: Bool) {
self.file = file
if calculateNow {
do {
let samples = try getSamples()
self.amplitude = getAmplitude(samples)
} catch {
// Error will be handled by caller
}
}
}
func getSamples() throws -> [Float] {
guard
let buffer = AVAudioPCMBuffer(
pcmFormat: file.processingFormat, frameCapacity: AVAudioFrameCount(file.length))
else {
let detailedError = AudioError.bufferConversion
Task {
await PlayolaErrorReporter.shared.reportError(
detailedError,
context:
"Failed to create PCM buffer | File format: \(file.processingFormat.description) | "
+ "Frame capacity: \(file.length)",
level: .error)
}
throw detailedError
}
do {
try file.read(into: buffer)
} catch {
// Enhance the error with context before throwing
Task {
await PlayolaErrorReporter.shared.reportError(
error,
context:
"Failed to read audio file into buffer | File: \(file.url.lastPathComponent) | "
+ "Format: \(file.processingFormat.description)",
level: .error)
}
throw error
}
guard let channelData = buffer.floatChannelData?.pointee else {
let error = AudioError.bufferConversion
Task {
await PlayolaErrorReporter.shared.reportError(
error,
context:
"Failed to get float channel data from buffer | Buffer length: \(buffer.frameLength)",
level: .error)
}
throw error
}
let floatArray = Array(UnsafeBufferPointer(start: channelData, count: Int(buffer.frameLength)))
return floatArray
}
func getAmplitude(_ samples: [Float]) -> Float? {
let absArray = samples.map { abs($0) }
return absArray.max()
}
}
// MARK: - Loudness helpers
extension AudioNormalizationCalculator {
/// Compute the dB offset required to reach target loudness from a given peak amplitude.
/// If `amplitude` is nil or non‑positive, returns 0 dB.
/// Formula: gain(dB) = -20 * log10(amplitude)
static func requiredDbOffsetDb(forAmplitude amplitude: Float?) -> Float {
guard let amplitude, amplitude > 0 else { return 0 }
return Float(-20.0 * log10(Double(amplitude)))
}
/// Instance convenience accessor that uses the calculator's measured amplitude.
var requiredDbOffsetDb: Float {
Self.requiredDbOffsetDb(forAmplitude: amplitude)
}
}