@javascriptcommon/react-native-track-player 4.1.22 → 4.1.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,285 +2,588 @@
2
2
  // EqualizerAudioTap.swift
3
3
  // SwiftAudioEx
4
4
  //
5
- // iOS Equalizer implementation using biquad peaking filters
5
+ // Professional 10-band parametric EQ with audio effects.
6
+ //
7
+ // Quality features:
8
+ // - Float64 arithmetic in all IIR paths (eliminates rounding artefacts)
9
+ // - Per-block coefficient smoothing (click-free parameter changes)
10
+ // - DC blocking filter (removes sub-sonic drift from cascaded IIRs)
11
+ // - Envelope-following true-peak limiter (transparent ceiling protection)
12
+ // - All-pass-based stereo virtualizer (natural phase-width, no comb filtering)
6
13
  //
7
14
 
8
15
  import Foundation
9
16
  import AVFoundation
10
- import Accelerate
11
17
 
12
- /// 10-band parametric equalizer using cascaded biquad filters
13
- /// Processes audio in real-time via MTAudioProcessingTap
14
18
  public class EqualizerAudioTap: AudioTap {
15
19
 
16
20
  // MARK: - Constants
17
21
 
18
- /// Standard 10-band equalizer frequencies (Hz)
22
+ /// 8-band equalizer frequencies (Hz), log-spaced for full-spectrum coverage
19
23
  public static let frequencies: [Float] = [
20
- 32, // Sub-bass
21
- 64, // Bass
22
- 125, // Low-bass
23
- 250, // Low-mid
24
- 500, // Mid
25
- 1000, // Mid
26
- 2000, // Upper-mid
27
- 4000, // Presence
28
- 8000, // Brilliance
29
- 16000 // Air
24
+ 60, 150, 400, 1000, 2500, 6000, 12000, 16000
30
25
  ]
26
+ public static let bandCount = 8
27
+
28
+ /// Q factor for peaking filters (standard for 1-octave graphic EQ)
29
+ private static let eqQ: Double = 1.41
30
+
31
+ /// Passthrough biquad coefficients [b0, b1, b2, a1, a2]
32
+ private static let kUnity: [Double] = [1, 0, 0, 0, 0]
31
33
 
32
- /// Number of equalizer bands
33
- public static let bandCount = 10
34
+ // MARK: - Parameter Lock
34
35
 
35
- /// Default Q factor for peaking filters (standard for graphic EQ)
36
- private static let defaultQ: Float = 1.41
36
+ /// Single lock protecting all mutable parameters shared between threads
37
+ private let paramLock = NSLock()
37
38
 
38
- // MARK: - Properties
39
+ // MARK: - EQ Bands
39
40
 
40
- /// Whether the equalizer is enabled
41
+ /// Whether the 10-band EQ is active
41
42
  public var isEnabled: Bool = true
42
43
 
43
- /// Gain values for each band in dB (-12 to +12)
44
+ /// Per-band gain in dB (-12 to +12), protected by paramLock
44
45
  private var _gains: [Float] = Array(repeating: 0, count: bandCount)
46
+ private var needsEQUpdate: Bool = true
45
47
 
46
- /// Thread-safe access to gains
47
- private let gainsLock = NSLock()
48
+ // MARK: - Bass Boost (always-positive low shelf)
48
49
 
49
- /// Current sample rate (set during prepare)
50
- private var sampleRate: Float = 44100
50
+ public var isBassBoostEnabled: Bool = false
51
+ public var bassBoostLevel: Float = 0.5
52
+ private static let bbFreq: Double = 150
53
+ private static let bbMinDB: Double = 6 // slider 0 = gentle warmth
54
+ private static let bbMaxDB: Double = 24 // slider 1 = full boost
55
+ private static let bbQ: Double = 0.8
56
+ private var needsBBUpdate: Bool = true
51
57
 
52
- /// Biquad filter coefficients for each band [b0, b1, b2, a1, a2]
53
- private var coefficients: [[Float]] = []
58
+ // MARK: - Loudness Enhancer (low shelf + high shelf)
54
59
 
55
- /// Filter state (delay elements) for each band, per channel
56
- /// Structure: [band][channel][z1, z2]
57
- private var filterStates: [[[Float]]] = []
60
+ public var isLoudnessEnabled: Bool = false
61
+ public var loudnessLevel: Float = 0.5
62
+ private static let lnLoFreq: Double = 200
63
+ private static let lnLoMaxDB: Double = 15
64
+ private static let lnHiFreq: Double = 3000
65
+ private static let lnHiMaxDB: Double = 10
66
+ private static let lnQ: Double = 0.7
67
+ private var needsLnUpdate: Bool = true
58
68
 
59
- /// Number of audio channels
60
- private var channelCount: Int = 2
69
+ // MARK: - Balance (L/R panning)
70
+
71
+ public var balance: Float = 0.0 // -1.0 (full left) to 1.0 (full right)
72
+
73
+ // MARK: - Virtualizer (cross-channel all-pass stereo widener)
61
74
 
62
- /// Flag to indicate coefficients need recalculation
63
- private var needsUpdate: Bool = true
75
+ public var isVirtualizerEnabled: Bool = false
76
+ public var virtualizerLevel: Float = 0.5
77
+ /// All-pass center frequencies — different per channel for cross-channel decorrelation
78
+ private static let apFreqsL: [Double] = [250, 630, 1500, 3200, 7500]
79
+ private static let apFreqsR: [Double] = [160, 420, 1000, 2200, 5000, 9500]
64
80
 
65
- // MARK: - Public API
81
+ // MARK: - Audio-Thread State (only accessed from process callback)
82
+
83
+ private var sampleRate: Double = 44100
84
+ private var channelCount: Int = 2
85
+
86
+ // EQ: current (smoothed) and target coefficients, plus filter states
87
+ private var eqCur: [[Double]] = [] // [band][5]
88
+ private var eqTgt: [[Double]] = [] // [band][5]
89
+ private var eqZ: [[[Double]]] = [] // [band][channel][2] (z1,z2)
90
+
91
+ // Bass boost
92
+ private var bbCur: [Double] = kUnity
93
+ private var bbTgt: [Double] = kUnity
94
+ private var bbZ: [[Double]] = [] // [channel][2]
95
+
96
+ // Loudness
97
+ private var lnLoCur: [Double] = kUnity
98
+ private var lnLoTgt: [Double] = kUnity
99
+ private var lnHiCur: [Double] = kUnity
100
+ private var lnHiTgt: [Double] = kUnity
101
+ private var lnLoZ: [[Double]] = [] // [channel][2]
102
+ private var lnHiZ: [[Double]] = []
103
+
104
+ // Virtualizer: first-order all-pass coefficients and states (per channel)
105
+ private var apCoeffsL: [Double] = [] // left channel sections
106
+ private var apCoeffsR: [Double] = [] // right channel sections
107
+ private var apStateL: [[Double]] = [] // [section][2: xPrev, yPrev]
108
+ private var apStateR: [[Double]] = [] // [section][2: xPrev, yPrev]
109
+
110
+ // DC blocker: y[n] = x[n] - x[n-1] + R*y[n-1]
111
+ private static let dcR: Double = 0.9995 // ~3.5 Hz cutoff at 44.1 kHz
112
+ private var dcXprev: [Double] = [] // per channel
113
+ private var dcYprev: [Double] = []
114
+
115
+ // Envelope-following peak limiter
116
+ private static let limThreshold: Double = 0.89 // ~ -1 dBFS
117
+ private var limGain: Double = 1.0
118
+ private var limAttCoeff: Double = 0 // computed from sample rate
119
+ private var limRelCoeff: Double = 0
120
+
121
+ // Coefficient smoothing alpha (computed from sample rate)
122
+ private var smoothAlpha: Double = 0.004
123
+
124
+ // Tap lifecycle tracking
125
+ private var activeTapCount: Int = 0
126
+
127
+ // ════════════════════════ Public API ═════════════════════════
66
128
 
67
129
  public override init() {
68
130
  super.init()
69
- // Initialize with flat response (all gains at 0)
70
- resetGains()
71
131
  }
72
132
 
73
- /// Set gain for a specific band
74
- /// - Parameters:
75
- /// - band: Band index (0-9)
76
- /// - gainDB: Gain in decibels (-12 to +12)
133
+ /// Set gain for a specific band in dB (-12 to +12)
77
134
  public func setGain(band: Int, gainDB: Float) {
78
- guard band >= 0 && band < Self.bandCount else { return }
79
-
80
- let clampedGain = max(-12, min(12, gainDB))
81
-
82
- gainsLock.lock()
83
- _gains[band] = clampedGain
84
- needsUpdate = true
85
- gainsLock.unlock()
135
+ guard band >= 0, band < Self.bandCount else { return }
136
+ paramLock.lock()
137
+ _gains[band] = max(-12, min(12, gainDB))
138
+ needsEQUpdate = true
139
+ paramLock.unlock()
86
140
  }
87
141
 
88
- /// Set gains for all bands at once
89
- /// - Parameter gains: Array of 10 gain values in dB
142
+ /// Set all 10 band gains at once
90
143
  public func setAllGains(_ gains: [Float]) {
91
144
  guard gains.count == Self.bandCount else { return }
92
-
93
- gainsLock.lock()
94
- for i in 0..<Self.bandCount {
95
- _gains[i] = max(-12, min(12, gains[i]))
96
- }
97
- needsUpdate = true
98
- gainsLock.unlock()
145
+ paramLock.lock()
146
+ for i in 0..<Self.bandCount { _gains[i] = max(-12, min(12, gains[i])) }
147
+ needsEQUpdate = true
148
+ paramLock.unlock()
99
149
  }
100
150
 
101
- /// Get current gain for a specific band
151
+ /// Get current gain for a band
102
152
  public func getGain(band: Int) -> Float {
103
- guard band >= 0 && band < Self.bandCount else { return 0 }
104
-
105
- gainsLock.lock()
106
- let gain = _gains[band]
107
- gainsLock.unlock()
108
- return gain
153
+ guard band >= 0, band < Self.bandCount else { return 0 }
154
+ paramLock.lock(); defer { paramLock.unlock() }
155
+ return _gains[band]
109
156
  }
110
157
 
111
158
  /// Get all current gains
112
159
  public func getAllGains() -> [Float] {
113
- gainsLock.lock()
114
- let gains = _gains
115
- gainsLock.unlock()
116
- return gains
160
+ paramLock.lock(); defer { paramLock.unlock() }
161
+ return _gains
117
162
  }
118
163
 
119
- /// Reset all gains to 0 (flat response)
164
+ /// Reset all gains to 0 dB (flat)
120
165
  public func resetGains() {
121
- gainsLock.lock()
166
+ paramLock.lock()
122
167
  _gains = Array(repeating: 0, count: Self.bandCount)
123
- needsUpdate = true
124
- gainsLock.unlock()
168
+ needsEQUpdate = true
169
+ paramLock.unlock()
170
+ }
171
+
172
+ /// Update bass boost intensity (0.0 = 6 dB, 1.0 = 24 dB)
173
+ public func updateBassBoostLevel(_ level: Float) {
174
+ paramLock.lock()
175
+ bassBoostLevel = max(0, min(1, level))
176
+ needsBBUpdate = true
177
+ paramLock.unlock()
178
+ }
179
+
180
+ /// Update loudness enhancer intensity (0.0 = off, 1.0 = full)
181
+ public func updateLoudnessLevel(_ level: Float) {
182
+ paramLock.lock()
183
+ loudnessLevel = max(0, min(1, level))
184
+ needsLnUpdate = true
185
+ paramLock.unlock()
125
186
  }
126
187
 
127
- // MARK: - AudioTap Overrides
188
+ /// Update virtualizer width (0.0 = subtle, 1.0 = maximum)
189
+ public func updateVirtualizerLevel(_ level: Float) {
190
+ virtualizerLevel = max(0, min(1, level))
191
+ }
192
+
193
+ // ═══════════════════ AudioTap Lifecycle ══════════════════════
128
194
 
129
195
  public override func initialize() {
130
- // Called when tap is attached
196
+ activeTapCount += 1
131
197
  }
132
198
 
133
199
  public override func finalize() {
134
- // Called when tap is detached
135
- filterStates = []
136
- coefficients = []
200
+ activeTapCount -= 1
201
+ if activeTapCount <= 0 {
202
+ eqCur = []; eqTgt = []; eqZ = []
203
+ bbZ = []; lnLoZ = []; lnHiZ = []
204
+ apStateL = []; apStateR = []; apCoeffsL = []; apCoeffsR = []
205
+ dcXprev = []; dcYprev = []
206
+ activeTapCount = 0
207
+ }
137
208
  }
138
209
 
139
210
  public override func prepare(description: AudioStreamBasicDescription) {
140
- sampleRate = Float(description.mSampleRate)
211
+ sampleRate = Double(description.mSampleRate)
141
212
  channelCount = Int(description.mChannelsPerFrame)
213
+ let ch = channelCount
214
+ let z2 = [Double](repeating: 0, count: 2)
215
+
216
+ // Smoothing alpha: ~5 ms time constant, adapted to sample rate
217
+ smoothAlpha = 1.0 - exp(-1.0 / (0.005 * sampleRate))
218
+
219
+ // ── EQ bands ──
220
+ eqCur = Array(repeating: Self.kUnity, count: Self.bandCount)
221
+ eqTgt = Array(repeating: Self.kUnity, count: Self.bandCount)
222
+ eqZ = Array(repeating: Array(repeating: z2, count: ch), count: Self.bandCount)
223
+ refreshEQTargets()
224
+ eqCur = eqTgt // snap to initial values (no smoothing ramp on first prepare)
225
+
226
+ // ── Bass boost ──
227
+ bbZ = Array(repeating: z2, count: ch)
228
+ refreshBBTarget()
229
+ bbCur = bbTgt
230
+
231
+ // ── Loudness ──
232
+ lnLoZ = Array(repeating: z2, count: ch)
233
+ lnHiZ = Array(repeating: z2, count: ch)
234
+ refreshLnTargets()
235
+ lnLoCur = lnLoTgt
236
+ lnHiCur = lnHiTgt
237
+
238
+ // ── Virtualizer all-pass (cross-channel) ──
239
+ apCoeffsL = Self.apFreqsL.map { f in
240
+ let omega = tan(Double.pi * f / sampleRate)
241
+ return (1.0 - omega) / (1.0 + omega)
242
+ }
243
+ apCoeffsR = Self.apFreqsR.map { f in
244
+ let omega = tan(Double.pi * f / sampleRate)
245
+ return (1.0 - omega) / (1.0 + omega)
246
+ }
247
+ apStateL = Array(repeating: z2, count: Self.apFreqsL.count)
248
+ apStateR = Array(repeating: z2, count: Self.apFreqsR.count)
249
+
250
+ // ── DC blocker ──
251
+ dcXprev = Array(repeating: 0, count: ch)
252
+ dcYprev = Array(repeating: 0, count: ch)
142
253
 
143
- // Initialize filter states for each band and channel
144
- filterStates = Array(
145
- repeating: Array(
146
- repeating: [0, 0], // z1, z2 delay elements
147
- count: channelCount
148
- ),
149
- count: Self.bandCount
150
- )
151
-
152
- // Calculate initial coefficients
153
- updateCoefficients()
254
+ // ── Limiter ──
255
+ limGain = 1.0
256
+ limAttCoeff = 1.0 - exp(-1.0 / (0.0005 * sampleRate)) // 0.5 ms attack
257
+ limRelCoeff = 1.0 - exp(-1.0 / (0.050 * sampleRate)) // 50 ms release
154
258
  }
155
259
 
156
260
  public override func unprepare() {
157
- // Reset filter states
158
- for band in 0..<filterStates.count {
159
- for channel in 0..<filterStates[band].count {
160
- filterStates[band][channel] = [0, 0]
161
- }
261
+ for b in 0..<eqZ.count {
262
+ for c in 0..<eqZ[b].count { eqZ[b][c] = [0, 0] }
162
263
  }
264
+ for c in 0..<bbZ.count { bbZ[c] = [0, 0] }
265
+ for c in 0..<lnLoZ.count { lnLoZ[c] = [0, 0] }
266
+ for c in 0..<lnHiZ.count { lnHiZ[c] = [0, 0] }
267
+ for s in 0..<apStateL.count { apStateL[s] = [0, 0] }
268
+ for s in 0..<apStateR.count { apStateR[s] = [0, 0] }
269
+ for c in 0..<dcXprev.count { dcXprev[c] = 0; dcYprev[c] = 0 }
270
+ limGain = 1.0
163
271
  }
164
272
 
273
+ // ══════════════════════ Processing ═══════════════════════════
274
+
165
275
  public override func process(numberOfFrames: Int, buffer: UnsafeMutableAudioBufferListPointer) {
166
- // Skip processing if disabled or no frames
167
- guard isEnabled && numberOfFrames > 0 else { return }
168
-
169
- // Check if coefficients need update
170
- gainsLock.lock()
171
- if needsUpdate {
172
- updateCoefficientsLocked()
173
- needsUpdate = false
276
+ guard numberOfFrames > 0 else { return }
277
+
278
+ let anyActive = isEnabled || isBassBoostEnabled || isLoudnessEnabled || isVirtualizerEnabled || balance != 0
279
+ guard anyActive else { return }
280
+
281
+ let nCh = min(buffer.count, channelCount)
282
+
283
+ // ── 1. Absorb pending parameter changes (briefly hold lock) ──
284
+ paramLock.lock()
285
+ if needsEQUpdate { refreshEQTargetsLocked(); needsEQUpdate = false }
286
+ if needsBBUpdate { refreshBBTargetLocked(); needsBBUpdate = false }
287
+ if needsLnUpdate { refreshLnTargetsLocked(); needsLnUpdate = false }
288
+ paramLock.unlock()
289
+
290
+ // ── 2. Smooth coefficients toward targets ──
291
+ // Uses exponential convergence equivalent to per-sample smoothing
292
+ let sf = 1.0 - pow(1.0 - smoothAlpha, Double(numberOfFrames))
293
+
294
+ if isEnabled {
295
+ smoothCoeffs2D(&eqCur, toward: eqTgt, factor: sf)
296
+ }
297
+ if isBassBoostEnabled { smoothCoeffs(&bbCur, toward: bbTgt, factor: sf) }
298
+ if isLoudnessEnabled {
299
+ smoothCoeffs(&lnLoCur, toward: lnLoTgt, factor: sf)
300
+ smoothCoeffs(&lnHiCur, toward: lnHiTgt, factor: sf)
174
301
  }
175
- let currentCoefficients = coefficients
176
- gainsLock.unlock()
177
-
178
- // Process each channel
179
- for channelIndex in 0..<min(buffer.count, channelCount) {
180
- guard let channelData = buffer[channelIndex].mData else { continue }
181
-
182
- let samples = channelData.assumingMemoryBound(to: Float.self)
183
-
184
- // Apply each band's filter in cascade
185
- for bandIndex in 0..<Self.bandCount {
186
- guard bandIndex < currentCoefficients.count else { continue }
187
-
188
- let coeff = currentCoefficients[bandIndex]
189
- guard coeff.count >= 5 else { continue }
190
-
191
- let b0 = coeff[0]
192
- let b1 = coeff[1]
193
- let b2 = coeff[2]
194
- let a1 = coeff[3]
195
- let a2 = coeff[4]
196
-
197
- // Get filter state for this band and channel
198
- var z1 = filterStates[bandIndex][channelIndex][0]
199
- var z2 = filterStates[bandIndex][channelIndex][1]
200
-
201
- // Process samples using Direct Form II Transposed
202
- for i in 0..<numberOfFrames {
203
- let input = samples[i]
204
- let output = b0 * input + z1
205
- z1 = b1 * input - a1 * output + z2
206
- z2 = b2 * input - a2 * output
207
- samples[i] = output
302
+
303
+ // ── 3. EQ bands (10 cascaded peaking filters, Float64) ──
304
+ if isEnabled {
305
+ for ch in 0..<nCh {
306
+ guard let data = buffer[ch].mData else { continue }
307
+ let samples = data.assumingMemoryBound(to: Float.self)
308
+ for band in 0..<Self.bandCount {
309
+ guard band < eqCur.count, band < eqZ.count, ch < eqZ[band].count else { continue }
310
+ biquadD(samples, numberOfFrames, eqCur[band], &eqZ[band][ch])
208
311
  }
312
+ }
313
+ }
314
+
315
+ // ── 4. Bass boost (low shelf, Float64) ──
316
+ if isBassBoostEnabled {
317
+ for ch in 0..<nCh {
318
+ guard ch < bbZ.count, let data = buffer[ch].mData else { continue }
319
+ biquadD(data.assumingMemoryBound(to: Float.self), numberOfFrames, bbCur, &bbZ[ch])
320
+ }
321
+ }
322
+
323
+ // ── 5. Loudness enhancer (low shelf + high shelf, Float64) ──
324
+ if isLoudnessEnabled {
325
+ for ch in 0..<nCh {
326
+ guard ch < lnLoZ.count, ch < lnHiZ.count,
327
+ let data = buffer[ch].mData else { continue }
328
+ let samples = data.assumingMemoryBound(to: Float.self)
329
+ biquadD(samples, numberOfFrames, lnLoCur, &lnLoZ[ch])
330
+ biquadD(samples, numberOfFrames, lnHiCur, &lnHiZ[ch])
331
+ }
332
+ }
333
+
334
+ // ── 6. Virtualizer (mid-side + cross-channel all-pass, stereo only) ──
335
+ if isVirtualizerEnabled && nCh >= 2
336
+ && apCoeffsL.count == Self.apFreqsL.count
337
+ && apCoeffsR.count == Self.apFreqsR.count
338
+ {
339
+ guard let lD = buffer[0].mData, let rD = buffer[1].mData else { return }
340
+ let L = lD.assumingMemoryBound(to: Float.self)
341
+ let R = rD.assumingMemoryBound(to: Float.self)
342
+ let lvl = Double(virtualizerLevel)
343
+ let width = 1.0 + lvl * 1.5 // 1.0 (normal) to 2.5 (wide)
344
+ let apMix = 0.5 + lvl * 0.5 // 0.5 (moderate) to 1.0 (full phase shift)
345
+
346
+ for i in 0..<numberOfFrames {
347
+ let l = Double(L[i]), r = Double(R[i])
348
+
349
+ // Mid-side decomposition + widening
350
+ let mid = (l + r) * 0.5
351
+ let side = (l - r) * 0.5
352
+ let wL = mid + side * width
353
+ let wR = mid - side * width
354
+
355
+ // Cascade all-pass on LEFT channel (creates phase shift at apFreqsL)
356
+ var sigL = wL
357
+ for s in 0..<apCoeffsL.count {
358
+ let a = apCoeffsL[s]
359
+ let y = a * sigL + apStateL[s][0] - a * apStateL[s][1]
360
+ apStateL[s][0] = sigL
361
+ apStateL[s][1] = y
362
+ sigL = y
363
+ }
364
+
365
+ // Cascade all-pass on RIGHT channel (different frequencies → apFreqsR)
366
+ var sigR = wR
367
+ for s in 0..<apCoeffsR.count {
368
+ let a = apCoeffsR[s]
369
+ let y = a * sigR + apStateR[s][0] - a * apStateR[s][1]
370
+ apStateR[s][0] = sigR
371
+ apStateR[s][1] = y
372
+ sigR = y
373
+ }
374
+
375
+ // Cross-channel decorrelation: each channel gets its own phase-shifted version
376
+ L[i] = Float(wL * (1.0 - apMix) + sigL * apMix)
377
+ R[i] = Float(wR * (1.0 - apMix) + sigR * apMix)
378
+ }
379
+ }
380
+
381
+ // ── 7. DC blocker (removes sub-sonic drift, all active channels) ──
382
+ for ch in 0..<nCh {
383
+ guard ch < dcXprev.count, let data = buffer[ch].mData else { continue }
384
+ let samples = data.assumingMemoryBound(to: Float.self)
385
+ var xp = dcXprev[ch], yp = dcYprev[ch]
386
+ let r = Self.dcR
387
+ for i in 0..<numberOfFrames {
388
+ let x = Double(samples[i])
389
+ let y = x - xp + r * yp
390
+ xp = x; yp = y
391
+ samples[i] = Float(y)
392
+ }
393
+ dcXprev[ch] = xp; dcYprev[ch] = yp
394
+ }
395
+
396
+ // ── 8. Balance (L/R panning, stereo only) ──
397
+ if balance != 0 && nCh >= 2 {
398
+ guard let lD = buffer[0].mData, let rD = buffer[1].mData else { return }
399
+ let L = lD.assumingMemoryBound(to: Float.self)
400
+ let R = rD.assumingMemoryBound(to: Float.self)
401
+ let bal = Double(max(-1, min(1, balance)))
402
+ let lGain = Float(min(1.0, 1.0 - bal))
403
+ let rGain = Float(min(1.0, 1.0 + bal))
404
+ for i in 0..<numberOfFrames {
405
+ L[i] *= lGain
406
+ R[i] *= rGain
407
+ }
408
+ }
209
409
 
210
- // Save filter state
211
- filterStates[bandIndex][channelIndex][0] = z1
212
- filterStates[bandIndex][channelIndex][1] = z2
410
+ // ── 9. True-peak limiter (linked stereo, transparent) ──
411
+ // Tracks signal peaks and applies smooth gain reduction only when needed.
412
+ // Does NOT color the audio below the threshold (gain stays 1.0).
413
+ let thresh = Self.limThreshold
414
+ let attC = limAttCoeff, relC = limRelCoeff
415
+
416
+ if nCh >= 2 {
417
+ guard let lD = buffer[0].mData, let rD = buffer[1].mData else { return }
418
+ let L = lD.assumingMemoryBound(to: Float.self)
419
+ let R = rD.assumingMemoryBound(to: Float.self)
420
+ for i in 0..<numberOfFrames {
421
+ let peak = max(abs(Double(L[i])), abs(Double(R[i])))
422
+ let tgt = peak > thresh ? thresh / peak : 1.0
423
+ limGain += (tgt - limGain) * (tgt < limGain ? attC : relC)
424
+ L[i] = Float(Double(L[i]) * limGain)
425
+ R[i] = Float(Double(R[i]) * limGain)
426
+ }
427
+ } else if nCh == 1 {
428
+ guard let data = buffer[0].mData else { return }
429
+ let S = data.assumingMemoryBound(to: Float.self)
430
+ for i in 0..<numberOfFrames {
431
+ let peak = abs(Double(S[i]))
432
+ let tgt = peak > thresh ? thresh / peak : 1.0
433
+ limGain += (tgt - limGain) * (tgt < limGain ? attC : relC)
434
+ S[i] = Float(Double(S[i]) * limGain)
213
435
  }
214
436
  }
215
437
  }
216
438
 
217
- // MARK: - Private Methods
439
+ // ═══════════════ Internal: Biquad Processing ════════════════
440
+
441
+ /// Process Float32 samples through a biquad filter using Float64 arithmetic.
442
+ /// Direct Form II Transposed for numerical stability.
443
+ private func biquadD(
444
+ _ samples: UnsafeMutablePointer<Float>, _ count: Int,
445
+ _ c: [Double], _ z: inout [Double]
446
+ ) {
447
+ guard c.count >= 5, z.count >= 2 else { return }
448
+ let b0 = c[0], b1 = c[1], b2 = c[2], a1 = c[3], a2 = c[4]
449
+ var z1 = z[0], z2 = z[1]
450
+
451
+ for i in 0..<count {
452
+ let x = Double(samples[i])
453
+ let y = b0 * x + z1
454
+ z1 = b1 * x - a1 * y + z2
455
+ z2 = b2 * x - a2 * y
456
+ samples[i] = Float(y)
457
+ }
458
+
459
+ // Flush denormals to zero (prevents FPU slowdown on some architectures)
460
+ if z1.magnitude < 1e-15 { z1 = 0 }
461
+ if z2.magnitude < 1e-15 { z2 = 0 }
462
+
463
+ z[0] = z1; z[1] = z2
464
+ }
465
+
466
+ // ═══════════════ Internal: Coefficient Smoothing ════════════
467
+
468
+ /// Smoothly interpolate a 1D coefficient vector toward target
469
+ private func smoothCoeffs(_ cur: inout [Double], toward tgt: [Double], factor sf: Double) {
470
+ for k in 0..<min(cur.count, tgt.count) {
471
+ cur[k] += (tgt[k] - cur[k]) * sf
472
+ }
473
+ }
218
474
 
219
- private func updateCoefficients() {
220
- gainsLock.lock()
221
- updateCoefficientsLocked()
222
- gainsLock.unlock()
475
+ /// Smoothly interpolate a 2D coefficient array toward target
476
+ private func smoothCoeffs2D(_ cur: inout [[Double]], toward tgt: [[Double]], factor sf: Double) {
477
+ for i in 0..<min(cur.count, tgt.count) {
478
+ for k in 0..<min(cur[i].count, tgt[i].count) {
479
+ cur[i][k] += (tgt[i][k] - cur[i][k]) * sf
480
+ }
481
+ }
223
482
  }
224
483
 
225
- /// Calculate biquad coefficients for all bands (must be called with lock held)
226
- private func updateCoefficientsLocked() {
227
- coefficients = []
484
+ // ═══════════════ Internal: Target Coefficient Calc ══════════
228
485
 
229
- for i in 0..<Self.bandCount {
230
- let freq = Self.frequencies[i]
231
- let gainDB = _gains[i]
486
+ private func refreshEQTargets() {
487
+ paramLock.lock()
488
+ refreshEQTargetsLocked()
489
+ paramLock.unlock()
490
+ }
232
491
 
233
- let coeff = calculatePeakingEQCoefficients(
234
- frequency: freq,
235
- gainDB: gainDB,
236
- q: Self.defaultQ,
237
- sampleRate: sampleRate
492
+ /// Must be called with paramLock held
493
+ private func refreshEQTargetsLocked() {
494
+ eqTgt = (0..<Self.bandCount).map { i in
495
+ Self.peakingEQ(
496
+ freq: Double(Self.frequencies[i]),
497
+ gainDB: Double(_gains[i]),
498
+ q: Self.eqQ, sr: sampleRate
238
499
  )
239
- coefficients.append(coeff)
240
500
  }
241
501
  }
242
502
 
243
- /// Calculate biquad coefficients for a peaking EQ filter
244
- /// Based on Audio EQ Cookbook by Robert Bristow-Johnson
245
- /// - Parameters:
246
- /// - frequency: Center frequency in Hz
247
- /// - gainDB: Gain in decibels
248
- /// - q: Q factor (bandwidth)
249
- /// - sampleRate: Sample rate in Hz
250
- /// - Returns: Coefficients [b0, b1, b2, a1, a2] (normalized by a0)
251
- private func calculatePeakingEQCoefficients(
252
- frequency: Float,
253
- gainDB: Float,
254
- q: Float,
255
- sampleRate: Float
256
- ) -> [Float] {
257
- // If gain is essentially 0, return unity (pass-through)
258
- if abs(gainDB) < 0.01 {
259
- return [1, 0, 0, 0, 0] // b0=1, rest=0 means y[n] = x[n]
260
- }
503
+ private func refreshBBTarget() {
504
+ paramLock.lock()
505
+ refreshBBTargetLocked()
506
+ paramLock.unlock()
507
+ }
261
508
 
262
- let A = pow(10, gainDB / 40) // sqrt(10^(dB/20))
263
- let w0 = 2 * Float.pi * frequency / sampleRate
264
- let cosW0 = cos(w0)
265
- let sinW0 = sin(w0)
266
- let alpha = sinW0 / (2 * q)
267
-
268
- // Peaking EQ coefficients
269
- let b0 = 1 + alpha * A
270
- let b1 = -2 * cosW0
271
- let b2 = 1 - alpha * A
272
- let a0 = 1 + alpha / A
273
- let a1 = -2 * cosW0
274
- let a2 = 1 - alpha / A
275
-
276
- // Normalize by a0
277
- return [
278
- b0 / a0,
279
- b1 / a0,
280
- b2 / a0,
281
- a1 / a0,
282
- a2 / a0
283
- ]
509
+ /// Must be called with paramLock held
510
+ private func refreshBBTargetLocked() {
511
+ let g = Self.bbMinDB + (Self.bbMaxDB - Self.bbMinDB) * Double(bassBoostLevel)
512
+ bbTgt = Self.lowShelf(freq: Self.bbFreq, gainDB: g, q: Self.bbQ, sr: sampleRate)
513
+ }
514
+
515
+ private func refreshLnTargets() {
516
+ paramLock.lock()
517
+ refreshLnTargetsLocked()
518
+ paramLock.unlock()
519
+ }
520
+
521
+ /// Must be called with paramLock held
522
+ private func refreshLnTargetsLocked() {
523
+ let lo = Self.lnLoMaxDB * Double(loudnessLevel)
524
+ let hi = Self.lnHiMaxDB * Double(loudnessLevel)
525
+ lnLoTgt = Self.lowShelf(freq: Self.lnLoFreq, gainDB: lo, q: Self.lnQ, sr: sampleRate)
526
+ lnHiTgt = Self.highShelf(freq: Self.lnHiFreq, gainDB: hi, q: Self.lnQ, sr: sampleRate)
527
+ }
528
+
529
+ // ═══════════════ Filter Design (Audio EQ Cookbook) ═══════════
530
+ // All calculations in Float64 for maximum precision.
531
+ // Reference: Robert Bristow-Johnson's Audio EQ Cookbook
532
+
533
+ /// Peaking EQ (parametric bell)
534
+ private static func peakingEQ(freq: Double, gainDB: Double, q: Double, sr: Double) -> [Double] {
535
+ if abs(gainDB) < 0.01 { return kUnity }
536
+ let A = pow(10.0, gainDB / 40.0)
537
+ let w0 = 2.0 * Double.pi * freq / sr
538
+ let cw = cos(w0), sw = sin(w0)
539
+ let al = sw / (2.0 * q)
540
+
541
+ let b0 = 1.0 + al * A
542
+ let b1 = -2.0 * cw
543
+ let b2 = 1.0 - al * A
544
+ let a0 = 1.0 + al / A
545
+ let a1 = -2.0 * cw
546
+ let a2 = 1.0 - al / A
547
+
548
+ return [b0/a0, b1/a0, b2/a0, a1/a0, a2/a0]
549
+ }
550
+
551
+ /// Low-shelf filter
552
+ private static func lowShelf(freq: Double, gainDB: Double, q: Double, sr: Double) -> [Double] {
553
+ if abs(gainDB) < 0.01 { return kUnity }
554
+ let A = pow(10.0, gainDB / 40.0)
555
+ let w0 = 2.0 * Double.pi * freq / sr
556
+ let cw = cos(w0), sw = sin(w0)
557
+ let al = sw / (2.0 * q)
558
+ let s2a = 2.0 * sqrt(A) * al
559
+
560
+ let b0 = A * ((A + 1) - (A - 1) * cw + s2a)
561
+ let b1 = 2 * A * ((A - 1) - (A + 1) * cw)
562
+ let b2 = A * ((A + 1) - (A - 1) * cw - s2a)
563
+ let a0 = (A + 1) + (A - 1) * cw + s2a
564
+ let a1 = -2 * ((A - 1) + (A + 1) * cw)
565
+ let a2 = (A + 1) + (A - 1) * cw - s2a
566
+
567
+ return [b0/a0, b1/a0, b2/a0, a1/a0, a2/a0]
568
+ }
569
+
570
+ /// High-shelf filter
571
+ private static func highShelf(freq: Double, gainDB: Double, q: Double, sr: Double) -> [Double] {
572
+ if abs(gainDB) < 0.01 { return kUnity }
573
+ let A = pow(10.0, gainDB / 40.0)
574
+ let w0 = 2.0 * Double.pi * freq / sr
575
+ let cw = cos(w0), sw = sin(w0)
576
+ let al = sw / (2.0 * q)
577
+ let s2a = 2.0 * sqrt(A) * al
578
+
579
+ let b0 = A * ((A + 1) + (A - 1) * cw + s2a)
580
+ let b1 = -2 * A * ((A - 1) + (A + 1) * cw)
581
+ let b2 = A * ((A + 1) + (A - 1) * cw - s2a)
582
+ let a0 = (A + 1) - (A - 1) * cw + s2a
583
+ let a1 = 2 * ((A - 1) - (A + 1) * cw)
584
+ let a2 = (A + 1) - (A - 1) * cw - s2a
585
+
586
+ return [b0/a0, b1/a0, b2/a0, a1/a0, a2/a0]
284
587
  }
285
588
  }
286
589
 
@@ -290,51 +593,81 @@ extension EqualizerAudioTap {
290
593
 
291
594
  /// Predefined equalizer presets
292
595
  public enum Preset: String, CaseIterable {
293
- case flat = "Flat"
294
- case rock = "Rock"
295
- case pop = "Pop"
296
- case jazz = "Jazz"
297
- case classical = "Classical"
298
- case hiphop = "Hip Hop"
299
- case electronic = "Electronic"
300
- case acoustic = "Acoustic"
301
- case bass = "Bass Boost"
302
- case treble = "Treble Boost"
303
- case vocal = "Vocal"
304
- case loudness = "Loudness"
305
-
306
- /// Gain values for each preset (10 bands)
596
+ case acoustic = "eqAcoustic"
597
+ case bass = "eqBassBooster"
598
+ case bassReducer = "eqBassReducer"
599
+ case classical = "eqClassical"
600
+ case dance = "eqDance"
601
+ case deep = "eqDeep"
602
+ case electronic = "eqElectronic"
603
+ case flat = "eqFlat"
604
+ case hiphop = "eqHipHop"
605
+ case jazz = "eqJazz"
606
+ case latin = "eqLatin"
607
+ case loudness = "eqLoudness"
608
+ case lounge = "eqLounge"
609
+ case piano = "eqPiano"
610
+ case pop = "eqPop"
611
+ case rnb = "eqRnb"
612
+ case rock = "eqRock"
613
+ case smallSpeakers = "eqSmallSpeakers"
614
+ case spokenWord = "eqSpokenWord"
615
+ case treble = "eqTrebleBooster"
616
+ case trebleReducer = "eqTrebleReducer"
617
+ case vocal = "eqVocalBooster"
618
+
619
+ /// Gain values per preset (8 bands: 60, 150, 400, 1K, 2.5K, 6K, 12K, 16K)
307
620
  public var gains: [Float] {
308
621
  switch self {
309
- case .flat:
310
- return [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
311
- case .rock:
312
- return [5, 4, 3, 1, -1, 0, 2, 3, 4, 4]
313
- case .pop:
314
- return [-1, 1, 3, 4, 3, 1, 0, 1, 2, 2]
315
- case .jazz:
316
- return [3, 2, 1, 2, -1, -1, 0, 1, 2, 3]
317
- case .classical:
318
- return [4, 3, 2, 1, -1, -1, 0, 2, 3, 4]
319
- case .hiphop:
320
- return [5, 5, 3, 1, -1, 0, 1, 0, 2, 3]
321
- case .electronic:
322
- return [4, 4, 2, 0, -2, -1, 0, 2, 4, 4]
323
622
  case .acoustic:
324
- return [3, 2, 1, 1, 0, 0, 1, 2, 2, 2]
623
+ return [ 3, 2, 1, 0, 1, 2, 2, 2]
325
624
  case .bass:
326
- return [6, 5, 4, 2, 0, 0, 0, 0, 0, 0]
625
+ return [ 6, 5, 3, 1, 0, 0, 0, 0]
626
+ case .bassReducer:
627
+ return [-6, -5, -3, -1, 0, 0, 0, 0]
628
+ case .classical:
629
+ return [ 4, 2, 0, -1, 0, 2, 3, 3]
630
+ case .dance:
631
+ return [ 5, 4, 1, 0, 2, 4, 3, 2]
632
+ case .deep:
633
+ return [ 5, 4, 2, 1, 0, -1, -2, -3]
634
+ case .electronic:
635
+ return [ 4, 3, 0, -1, 0, 3, 4, 4]
636
+ case .flat:
637
+ return [ 0, 0, 0, 0, 0, 0, 0, 0]
638
+ case .hiphop:
639
+ return [ 5, 4, 2, 0, -1, 1, 2, 3]
640
+ case .jazz:
641
+ return [ 3, 2, 1, 0, -1, 1, 2, 2]
642
+ case .latin:
643
+ return [ 4, 3, 0, -1, 0, 2, 4, 3]
644
+ case .loudness:
645
+ return [ 5, 3, 0, -2, 0, 2, 4, 4]
646
+ case .lounge:
647
+ return [-1, 1, 2, 1, 0, -1, 1, 1]
648
+ case .piano:
649
+ return [ 1, 0, 1, 2, 3, 2, 2, 1]
650
+ case .pop:
651
+ return [-1, 1, 3, 3, 2, 1, 1, 1]
652
+ case .rnb:
653
+ return [ 5, 4, 2, 0, -1, 1, 2, 2]
654
+ case .rock:
655
+ return [ 4, 3, 0, -1, 1, 3, 4, 3]
656
+ case .smallSpeakers:
657
+ return [ 5, 4, 3, 1, 0, -1, 2, 3]
658
+ case .spokenWord:
659
+ return [-2, 0, 1, 3, 4, 3, 1, -1]
327
660
  case .treble:
328
- return [0, 0, 0, 0, 0, 1, 2, 4, 5, 6]
661
+ return [ 0, 0, 0, 0, 1, 3, 5, 6]
662
+ case .trebleReducer:
663
+ return [ 0, 0, 0, 0, -1, -3, -5, -6]
329
664
  case .vocal:
330
- return [-2, -1, 0, 2, 4, 4, 3, 1, 0, -1]
331
- case .loudness:
332
- return [5, 4, 2, 0, -2, -2, 0, 2, 4, 5]
665
+ return [-2, -1, 1, 3, 4, 3, 1, 0]
333
666
  }
334
667
  }
335
668
  }
336
669
 
337
- /// Apply a preset
670
+ /// Apply a built-in preset
338
671
  public func applyPreset(_ preset: Preset) {
339
672
  setAllGains(preset.gains)
340
673
  }