@javascriptcommon/react-native-track-player 4.1.23 → 4.1.25

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.
@@ -0,0 +1,517 @@
1
+ package com.lovegaoshi.kotlinaudio.processors
2
+
3
+ import androidx.media3.common.C
4
+ import androidx.media3.common.audio.AudioProcessor
5
+ import androidx.media3.common.audio.BaseAudioProcessor
6
+ import androidx.media3.common.util.UnstableApi
7
+ import java.nio.ByteBuffer
8
+ import kotlin.math.*
9
+
10
+ /**
11
+ * Software audio effects processor matching iOS EqualizerAudioTap.
12
+ * All processing in Float64 for maximum precision.
13
+ *
14
+ * Features:
15
+ * - 8-band parametric EQ (peaking biquad filters)
16
+ * - Bass boost (low shelf biquad)
17
+ * - Loudness enhancer (low shelf + high shelf biquad)
18
+ * - Stereo virtualizer (mid-side + cross-channel all-pass)
19
+ * - Per-block coefficient smoothing (click-free parameter changes)
20
+ * - DC blocking filter (removes sub-sonic drift)
21
+ * - Envelope-following true-peak limiter
22
+ * - Denormal flush (prevents FPU slowdown on ARM)
23
+ */
24
+ @UnstableApi
25
+ class EqualizerAudioProcessor : BaseAudioProcessor() {
26
+
27
+ companion object {
28
+ // ── EQ ──
29
+ const val BAND_COUNT = 8
30
+ val FREQUENCIES = floatArrayOf(60f, 150f, 400f, 1000f, 2500f, 6000f, 12000f, 16000f)
31
+ private const val EQ_Q = 1.4
32
+
33
+ // ── Bass Boost (low shelf) ──
34
+ private const val BB_FREQ = 150.0
35
+ private const val BB_MIN_DB = 6.0 // slider 0 = gentle warmth
36
+ private const val BB_MAX_DB = 24.0 // slider 1 = full boost
37
+ private const val BB_Q = 0.8
38
+
39
+ // ── Loudness Enhancer (low shelf + high shelf) ──
40
+ private const val LN_LO_FREQ = 200.0
41
+ private const val LN_LO_MAX_DB = 15.0
42
+ private const val LN_HI_FREQ = 3000.0
43
+ private const val LN_HI_MAX_DB = 10.0
44
+ private const val LN_Q = 0.7
45
+
46
+ // ── Virtualizer (cross-channel all-pass) ──
47
+ private val AP_FREQS_L = doubleArrayOf(250.0, 630.0, 1500.0, 3200.0, 7500.0)
48
+ private val AP_FREQS_R = doubleArrayOf(160.0, 420.0, 1000.0, 2200.0, 5000.0, 9500.0)
49
+
50
+ // ── Processing constants ──
51
+ private const val SMOOTH_ALPHA = 0.008
52
+ private const val DC_R = 0.9995 // ~3.5 Hz cutoff at 44.1 kHz
53
+ private const val LIM_THRESHOLD = 0.89 // ~ -1 dBFS
54
+ private const val DENORM_THRESH = 1e-15
55
+
56
+ private val UNITY = doubleArrayOf(1.0, 0.0, 0.0, 0.0, 0.0)
57
+
58
+ // ═══════════ Filter Design (Audio EQ Cookbook) ═══════════
59
+
60
+ fun peakingEQ(freq: Double, gainDB: Double, q: Double, sr: Double): DoubleArray {
61
+ if (abs(gainDB) < 0.01) return UNITY.copyOf()
62
+ val A = 10.0.pow(gainDB / 40.0)
63
+ val w0 = 2.0 * PI * freq / sr
64
+ val sw = sin(w0); val cw = cos(w0)
65
+ val al = sw / (2.0 * q)
66
+ val b0 = 1.0 + al * A; val b1 = -2.0 * cw; val b2 = 1.0 - al * A
67
+ val a0 = 1.0 + al / A; val a1 = -2.0 * cw; val a2 = 1.0 - al / A
68
+ return doubleArrayOf(b0/a0, b1/a0, b2/a0, a1/a0, a2/a0)
69
+ }
70
+
71
+ fun lowShelf(freq: Double, gainDB: Double, q: Double, sr: Double): DoubleArray {
72
+ if (abs(gainDB) < 0.01) return UNITY.copyOf()
73
+ val A = 10.0.pow(gainDB / 40.0)
74
+ val w0 = 2.0 * PI * freq / sr
75
+ val cw = cos(w0); val sw = sin(w0)
76
+ val al = sw / (2.0 * q)
77
+ val s2a = 2.0 * sqrt(A) * al
78
+ val b0 = A * ((A+1) - (A-1)*cw + s2a)
79
+ val b1 = 2*A * ((A-1) - (A+1)*cw)
80
+ val b2 = A * ((A+1) - (A-1)*cw - s2a)
81
+ val a0 = (A+1) + (A-1)*cw + s2a
82
+ val a1 = -2.0 * ((A-1) + (A+1)*cw)
83
+ val a2 = (A+1) + (A-1)*cw - s2a
84
+ return doubleArrayOf(b0/a0, b1/a0, b2/a0, a1/a0, a2/a0)
85
+ }
86
+
87
+ fun highShelf(freq: Double, gainDB: Double, q: Double, sr: Double): DoubleArray {
88
+ if (abs(gainDB) < 0.01) return UNITY.copyOf()
89
+ val A = 10.0.pow(gainDB / 40.0)
90
+ val w0 = 2.0 * PI * freq / sr
91
+ val cw = cos(w0); val sw = sin(w0)
92
+ val al = sw / (2.0 * q)
93
+ val s2a = 2.0 * sqrt(A) * al
94
+ val b0 = A * ((A+1) + (A-1)*cw + s2a)
95
+ val b1 = -2*A * ((A-1) + (A+1)*cw)
96
+ val b2 = A * ((A+1) + (A-1)*cw - s2a)
97
+ val a0 = (A+1) - (A-1)*cw + s2a
98
+ val a1 = 2.0 * ((A-1) - (A+1)*cw)
99
+ val a2 = (A+1) - (A-1)*cw - s2a
100
+ return doubleArrayOf(b0/a0, b1/a0, b2/a0, a1/a0, a2/a0)
101
+ }
102
+ }
103
+
104
+ // ═══════════════════ Public state ═══════════════════
105
+
106
+ // EQ
107
+ @Volatile var isEnabled = false
108
+ private val gains = FloatArray(BAND_COUNT)
109
+
110
+ // Bass Boost
111
+ @Volatile var isBassBoostEnabled = false
112
+ @Volatile var bassBoostLevel = 0.5f // 0..1
113
+
114
+ // Loudness
115
+ @Volatile var isLoudnessEnabled = false
116
+ @Volatile var loudnessLevel = 0.5f // 0..1
117
+
118
+ // Virtualizer
119
+ @Volatile var isVirtualizerEnabled = false
120
+ @Volatile var virtualizerLevel = 0.5f // 0..1
121
+
122
+ // ═══════════════════ Internal state ═══════════════════
123
+
124
+ private var sampleRate = 44100.0
125
+ private var channelCount = 2
126
+
127
+ // EQ coefficients: [band][5]
128
+ private var eqCur = Array(BAND_COUNT) { UNITY.copyOf() }
129
+ private var eqTgt = Array(BAND_COUNT) { UNITY.copyOf() }
130
+ private var eqZ = Array(BAND_COUNT) { Array(2) { doubleArrayOf(0.0, 0.0) } }
131
+
132
+ // Bass boost coefficients
133
+ private var bbCur = UNITY.copyOf()
134
+ private var bbTgt = UNITY.copyOf()
135
+ private var bbZ = Array(2) { doubleArrayOf(0.0, 0.0) }
136
+
137
+ // Loudness: low shelf + high shelf
138
+ private var lnLoCur = UNITY.copyOf()
139
+ private var lnLoTgt = UNITY.copyOf()
140
+ private var lnHiCur = UNITY.copyOf()
141
+ private var lnHiTgt = UNITY.copyOf()
142
+ private var lnLoZ = Array(2) { doubleArrayOf(0.0, 0.0) }
143
+ private var lnHiZ = Array(2) { doubleArrayOf(0.0, 0.0) }
144
+
145
+ // Virtualizer: first-order all-pass coefficients and states
146
+ private var apCoeffsL = DoubleArray(0)
147
+ private var apCoeffsR = DoubleArray(0)
148
+ private var apStateL = Array(0) { doubleArrayOf(0.0, 0.0) }
149
+ private var apStateR = Array(0) { doubleArrayOf(0.0, 0.0) }
150
+
151
+ // DC blocker
152
+ private var dcXprev = DoubleArray(2)
153
+ private var dcYprev = DoubleArray(2)
154
+
155
+ // Limiter
156
+ private var limGain = 1.0
157
+ private var limAttCoeff = 0.0
158
+ private var limRelCoeff = 0.0
159
+
160
+ // ═══════════════════ EQ API ═══════════════════
161
+
162
+ fun setGain(band: Int, gainDB: Float) {
163
+ if (band !in 0 until BAND_COUNT) return
164
+ gains[band] = gainDB.coerceIn(-12f, 12f)
165
+ eqTgt[band] = peakingEQ(FREQUENCIES[band].toDouble(), gains[band].toDouble(), EQ_Q, sampleRate)
166
+ }
167
+
168
+ fun setAllGains(gainsDB: List<Float>) {
169
+ for (i in 0 until minOf(gainsDB.size, BAND_COUNT)) {
170
+ gains[i] = gainsDB[i].coerceIn(-12f, 12f)
171
+ eqTgt[i] = peakingEQ(FREQUENCIES[i].toDouble(), gains[i].toDouble(), EQ_Q, sampleRate)
172
+ }
173
+ }
174
+
175
+ fun getAllGains(): FloatArray = gains.copyOf()
176
+
177
+ fun resetGains() {
178
+ for (i in 0 until BAND_COUNT) {
179
+ gains[i] = 0f
180
+ eqTgt[i] = UNITY.copyOf()
181
+ }
182
+ }
183
+
184
+ // ═══════════════════ Bass Boost API ═══════════════════
185
+
186
+ fun updateBassBoostLevel(level: Float) {
187
+ bassBoostLevel = level.coerceIn(0f, 1f)
188
+ val g = BB_MIN_DB + (BB_MAX_DB - BB_MIN_DB) * bassBoostLevel.toDouble()
189
+ bbTgt = lowShelf(BB_FREQ, g, BB_Q, sampleRate)
190
+ }
191
+
192
+ // ═══════════════════ Loudness API ═══════════════════
193
+
194
+ fun updateLoudnessLevel(level: Float) {
195
+ loudnessLevel = level.coerceIn(0f, 1f)
196
+ val lo = LN_LO_MAX_DB * loudnessLevel.toDouble()
197
+ val hi = LN_HI_MAX_DB * loudnessLevel.toDouble()
198
+ lnLoTgt = lowShelf(LN_LO_FREQ, lo, LN_Q, sampleRate)
199
+ lnHiTgt = highShelf(LN_HI_FREQ, hi, LN_Q, sampleRate)
200
+ }
201
+
202
+ // ═══════════════════ Virtualizer API ═══════════════════
203
+
204
+ fun updateVirtualizerLevel(level: Float) {
205
+ virtualizerLevel = level.coerceIn(0f, 1f)
206
+ }
207
+
208
+ // ═══════════════════ Pipeline ═══════════════════
209
+
210
+ override fun onConfigure(inputAudioFormat: AudioProcessor.AudioFormat): AudioProcessor.AudioFormat {
211
+ if (inputAudioFormat.encoding == C.ENCODING_PCM_16BIT ||
212
+ inputAudioFormat.encoding == C.ENCODING_PCM_FLOAT
213
+ ) {
214
+ sampleRate = inputAudioFormat.sampleRate.toDouble()
215
+ channelCount = inputAudioFormat.channelCount
216
+ val nCh = channelCount
217
+
218
+ // EQ
219
+ for (i in 0 until BAND_COUNT) {
220
+ eqTgt[i] = peakingEQ(FREQUENCIES[i].toDouble(), gains[i].toDouble(), EQ_Q, sampleRate)
221
+ eqCur[i] = eqTgt[i].copyOf()
222
+ }
223
+ eqZ = Array(BAND_COUNT) { Array(nCh) { doubleArrayOf(0.0, 0.0) } }
224
+
225
+ // Bass boost
226
+ if (isBassBoostEnabled) {
227
+ val g = BB_MIN_DB + (BB_MAX_DB - BB_MIN_DB) * bassBoostLevel.toDouble()
228
+ bbTgt = lowShelf(BB_FREQ, g, BB_Q, sampleRate)
229
+ }
230
+ bbCur = bbTgt.copyOf()
231
+ bbZ = Array(nCh) { doubleArrayOf(0.0, 0.0) }
232
+
233
+ // Loudness
234
+ if (isLoudnessEnabled) {
235
+ lnLoTgt = lowShelf(LN_LO_FREQ, LN_LO_MAX_DB * loudnessLevel.toDouble(), LN_Q, sampleRate)
236
+ lnHiTgt = highShelf(LN_HI_FREQ, LN_HI_MAX_DB * loudnessLevel.toDouble(), LN_Q, sampleRate)
237
+ }
238
+ lnLoCur = lnLoTgt.copyOf(); lnHiCur = lnHiTgt.copyOf()
239
+ lnLoZ = Array(nCh) { doubleArrayOf(0.0, 0.0) }
240
+ lnHiZ = Array(nCh) { doubleArrayOf(0.0, 0.0) }
241
+
242
+ // Virtualizer all-pass coefficients
243
+ apCoeffsL = DoubleArray(AP_FREQS_L.size) { i ->
244
+ val omega = tan(PI * AP_FREQS_L[i] / sampleRate)
245
+ (1.0 - omega) / (1.0 + omega)
246
+ }
247
+ apCoeffsR = DoubleArray(AP_FREQS_R.size) { i ->
248
+ val omega = tan(PI * AP_FREQS_R[i] / sampleRate)
249
+ (1.0 - omega) / (1.0 + omega)
250
+ }
251
+ apStateL = Array(AP_FREQS_L.size) { doubleArrayOf(0.0, 0.0) }
252
+ apStateR = Array(AP_FREQS_R.size) { doubleArrayOf(0.0, 0.0) }
253
+
254
+ // DC blocker
255
+ dcXprev = DoubleArray(nCh)
256
+ dcYprev = DoubleArray(nCh)
257
+
258
+ // Limiter
259
+ limGain = 1.0
260
+ limAttCoeff = 1.0 - exp(-1.0 / (0.0005 * sampleRate))
261
+ limRelCoeff = 1.0 - exp(-1.0 / (0.050 * sampleRate))
262
+
263
+ return inputAudioFormat
264
+ }
265
+ return AudioProcessor.AudioFormat.NOT_SET
266
+ }
267
+
268
+ override fun queueInput(inputBuffer: ByteBuffer) {
269
+ val nCh = channelCount
270
+ val size = inputBuffer.remaining()
271
+ val output = replaceOutputBuffer(size)
272
+
273
+ val anyActive = isEnabled || isBassBoostEnabled || isLoudnessEnabled || isVirtualizerEnabled
274
+
275
+ if (!anyActive) {
276
+ // Pass-through
277
+ when (inputAudioFormat.encoding) {
278
+ C.ENCODING_PCM_16BIT -> while (inputBuffer.hasRemaining()) output.putShort(inputBuffer.short)
279
+ C.ENCODING_PCM_FLOAT -> while (inputBuffer.hasRemaining()) output.putFloat(inputBuffer.float)
280
+ }
281
+ output.flip()
282
+ return
283
+ }
284
+
285
+ // Smooth all coefficients toward targets
286
+ if (isEnabled) smoothCoeffs2D(eqCur, eqTgt)
287
+ if (isBassBoostEnabled) smoothCoeffs(bbCur, bbTgt)
288
+ if (isLoudnessEnabled) {
289
+ smoothCoeffs(lnLoCur, lnLoTgt)
290
+ smoothCoeffs(lnHiCur, lnHiTgt)
291
+ }
292
+
293
+ when (inputAudioFormat.encoding) {
294
+ C.ENCODING_PCM_16BIT -> processInt16(inputBuffer, output, nCh, size)
295
+ C.ENCODING_PCM_FLOAT -> processFloat32(inputBuffer, output, nCh, size)
296
+ }
297
+ output.flip()
298
+ }
299
+
300
+ // ═══════════════ Processing: Float32 path ═══════════════
301
+
302
+ private fun processFloat32(input: ByteBuffer, output: ByteBuffer, nCh: Int, size: Int) {
303
+ val frameCount = size / (4 * nCh)
304
+
305
+ // Read all samples into a working buffer [channel][frame]
306
+ val buf = Array(nCh) { DoubleArray(frameCount) }
307
+ for (frame in 0 until frameCount) {
308
+ for (ch in 0 until nCh) {
309
+ buf[ch][frame] = input.float.toDouble()
310
+ }
311
+ }
312
+
313
+ // 1. EQ bands
314
+ if (isEnabled) {
315
+ for (ch in 0 until nCh) {
316
+ val chIdx = ch.coerceAtMost(eqZ[0].size - 1)
317
+ for (band in 0 until BAND_COUNT) {
318
+ biquadBlock(buf[ch], frameCount, eqCur[band], eqZ[band][chIdx])
319
+ }
320
+ }
321
+ }
322
+
323
+ // 2. Bass boost
324
+ if (isBassBoostEnabled) {
325
+ for (ch in 0 until nCh) {
326
+ val chIdx = ch.coerceAtMost(bbZ.size - 1)
327
+ biquadBlock(buf[ch], frameCount, bbCur, bbZ[chIdx])
328
+ }
329
+ }
330
+
331
+ // 3. Loudness (low shelf + high shelf)
332
+ if (isLoudnessEnabled) {
333
+ for (ch in 0 until nCh) {
334
+ val chIdx = ch.coerceAtMost(lnLoZ.size - 1)
335
+ biquadBlock(buf[ch], frameCount, lnLoCur, lnLoZ[chIdx])
336
+ biquadBlock(buf[ch], frameCount, lnHiCur, lnHiZ[chIdx])
337
+ }
338
+ }
339
+
340
+ // 4. Virtualizer (stereo only)
341
+ if (isVirtualizerEnabled && nCh >= 2) {
342
+ applyVirtualizer(buf[0], buf[1], frameCount)
343
+ }
344
+
345
+ // 5. DC blocker
346
+ for (ch in 0 until nCh) {
347
+ val chIdx = ch.coerceAtMost(dcXprev.size - 1)
348
+ applyDCBlock(buf[ch], frameCount, chIdx)
349
+ }
350
+
351
+ // 6. True-peak limiter + write output
352
+ for (frame in 0 until frameCount) {
353
+ var peak = 0.0
354
+ for (ch in 0 until nCh) peak = max(peak, abs(buf[ch][frame]))
355
+ val tgt = if (peak > LIM_THRESHOLD) LIM_THRESHOLD / peak else 1.0
356
+ limGain += (tgt - limGain) * if (tgt < limGain) limAttCoeff else limRelCoeff
357
+ val g = limGain
358
+ for (ch in 0 until nCh) {
359
+ output.putFloat((buf[ch][frame] * g).toFloat())
360
+ }
361
+ }
362
+ }
363
+
364
+ // ═══════════════ Processing: Int16 path ═══════════════
365
+
366
+ private fun processInt16(input: ByteBuffer, output: ByteBuffer, nCh: Int, size: Int) {
367
+ val frameCount = size / (2 * nCh)
368
+
369
+ val buf = Array(nCh) { DoubleArray(frameCount) }
370
+ for (frame in 0 until frameCount) {
371
+ for (ch in 0 until nCh) {
372
+ buf[ch][frame] = input.short.toDouble() / 32768.0
373
+ }
374
+ }
375
+
376
+ // 1. EQ bands
377
+ if (isEnabled) {
378
+ for (ch in 0 until nCh) {
379
+ val chIdx = ch.coerceAtMost(eqZ[0].size - 1)
380
+ for (band in 0 until BAND_COUNT) {
381
+ biquadBlock(buf[ch], frameCount, eqCur[band], eqZ[band][chIdx])
382
+ }
383
+ }
384
+ }
385
+
386
+ // 2. Bass boost
387
+ if (isBassBoostEnabled) {
388
+ for (ch in 0 until nCh) {
389
+ val chIdx = ch.coerceAtMost(bbZ.size - 1)
390
+ biquadBlock(buf[ch], frameCount, bbCur, bbZ[chIdx])
391
+ }
392
+ }
393
+
394
+ // 3. Loudness
395
+ if (isLoudnessEnabled) {
396
+ for (ch in 0 until nCh) {
397
+ val chIdx = ch.coerceAtMost(lnLoZ.size - 1)
398
+ biquadBlock(buf[ch], frameCount, lnLoCur, lnLoZ[chIdx])
399
+ biquadBlock(buf[ch], frameCount, lnHiCur, lnHiZ[chIdx])
400
+ }
401
+ }
402
+
403
+ // 4. Virtualizer (stereo only)
404
+ if (isVirtualizerEnabled && nCh >= 2) {
405
+ applyVirtualizer(buf[0], buf[1], frameCount)
406
+ }
407
+
408
+ // 5. DC blocker
409
+ for (ch in 0 until nCh) {
410
+ val chIdx = ch.coerceAtMost(dcXprev.size - 1)
411
+ applyDCBlock(buf[ch], frameCount, chIdx)
412
+ }
413
+
414
+ // 6. True-peak limiter + write output
415
+ for (frame in 0 until frameCount) {
416
+ var peak = 0.0
417
+ for (ch in 0 until nCh) peak = max(peak, abs(buf[ch][frame]))
418
+ val tgt = if (peak > LIM_THRESHOLD) LIM_THRESHOLD / peak else 1.0
419
+ limGain += (tgt - limGain) * if (tgt < limGain) limAttCoeff else limRelCoeff
420
+ val g = limGain
421
+ for (ch in 0 until nCh) {
422
+ output.putShort((buf[ch][frame] * g * 32768.0).coerceIn(-32768.0, 32767.0).toInt().toShort())
423
+ }
424
+ }
425
+ }
426
+
427
+ // ═══════════════ Virtualizer ═══════════════
428
+
429
+ private fun applyVirtualizer(L: DoubleArray, R: DoubleArray, count: Int) {
430
+ val lvl = virtualizerLevel.toDouble()
431
+ val width = 1.0 + lvl * 1.5 // 1.0 (normal) to 2.5 (wide)
432
+ val apMix = 0.5 + lvl * 0.5 // 0.5 (moderate) to 1.0 (full phase shift)
433
+
434
+ for (i in 0 until count) {
435
+ val l = L[i]; val r = R[i]
436
+
437
+ // Mid-side decomposition + widening
438
+ val mid = (l + r) * 0.5
439
+ val side = (l - r) * 0.5
440
+ val wL = mid + side * width
441
+ val wR = mid - side * width
442
+
443
+ // Cascade all-pass on LEFT channel
444
+ var sigL = wL
445
+ for (s in apCoeffsL.indices) {
446
+ val a = apCoeffsL[s]
447
+ val y = a * sigL + apStateL[s][0] - a * apStateL[s][1]
448
+ apStateL[s][0] = sigL
449
+ apStateL[s][1] = y
450
+ sigL = y
451
+ }
452
+
453
+ // Cascade all-pass on RIGHT channel
454
+ var sigR = wR
455
+ for (s in apCoeffsR.indices) {
456
+ val a = apCoeffsR[s]
457
+ val y = a * sigR + apStateR[s][0] - a * apStateR[s][1]
458
+ apStateR[s][0] = sigR
459
+ apStateR[s][1] = y
460
+ sigR = y
461
+ }
462
+
463
+ // Cross-channel decorrelation
464
+ L[i] = wL * (1.0 - apMix) + sigL * apMix
465
+ R[i] = wR * (1.0 - apMix) + sigR * apMix
466
+ }
467
+ }
468
+
469
+ // ═══════════════ DC Blocker ═══════════════
470
+
471
+ private fun applyDCBlock(samples: DoubleArray, count: Int, ch: Int) {
472
+ var xp = dcXprev[ch]; var yp = dcYprev[ch]
473
+ for (i in 0 until count) {
474
+ val x = samples[i]
475
+ val y = x - xp + DC_R * yp
476
+ xp = x; yp = y
477
+ samples[i] = y
478
+ }
479
+ dcXprev[ch] = xp; dcYprev[ch] = yp
480
+ }
481
+
482
+ // ═══════════════ Biquad processing ═══════════════
483
+
484
+ /** Process a block of Float64 samples through a biquad filter (Direct Form II Transposed) */
485
+ private fun biquadBlock(samples: DoubleArray, count: Int, c: DoubleArray, z: DoubleArray) {
486
+ val b0 = c[0]; val b1 = c[1]; val b2 = c[2]; val a1 = c[3]; val a2 = c[4]
487
+ var z1 = z[0]; var z2 = z[1]
488
+ for (i in 0 until count) {
489
+ val x = samples[i]
490
+ val y = b0 * x + z1
491
+ z1 = b1 * x - a1 * y + z2
492
+ z2 = b2 * x - a2 * y
493
+ samples[i] = y
494
+ }
495
+ // Flush denormals
496
+ if (abs(z1) < DENORM_THRESH) z1 = 0.0
497
+ if (abs(z2) < DENORM_THRESH) z2 = 0.0
498
+ z[0] = z1; z[1] = z2
499
+ }
500
+
501
+ // ═══════════════ Coefficient smoothing ═══════════════
502
+
503
+ private fun smoothCoeffs(cur: DoubleArray, tgt: DoubleArray) {
504
+ for (k in cur.indices) {
505
+ cur[k] += SMOOTH_ALPHA * (tgt[k] - cur[k])
506
+ }
507
+ }
508
+
509
+ private fun smoothCoeffs2D(cur: Array<DoubleArray>, tgt: Array<DoubleArray>) {
510
+ for (i in cur.indices) {
511
+ val c = cur[i]; val t = tgt[i]
512
+ for (k in c.indices) {
513
+ c[k] += SMOOTH_ALPHA * (t[k] - c[k])
514
+ }
515
+ }
516
+ }
517
+ }
@@ -140,8 +140,11 @@ public class RNTrackPlayer: NSObject, AudioSessionControllerDelegate {
140
140
  if let fftLength = config["useFFTProcessor"] as? Int {
141
141
  player.audioTap = WaveformAudioTap(mFFTLength: fftLength, mEmit: {data in
142
142
  self.emit(event:EventType.FFTUpdated, body:data)})
143
+ } else {
144
+ // Always attach equalizer tap — transparent pass-through when no effects are active.
145
+ // This avoids audio glitches from re-attaching the tap when effects are toggled.
146
+ player.audioTap = equalizerTap
143
147
  }
144
- // Note: Equalizer tap is NOT attached by default. Call setEqualizerEnabled(true) to enable.
145
148
 
146
149
  // configure buffer size
147
150
  if let bufferDuration = config["minBuffer"] as? TimeInterval {
@@ -822,7 +825,6 @@ public class RNTrackPlayer: NSObject, AudioSessionControllerDelegate {
822
825
  lastIndex: Int?,
823
826
  lastPosition: Double?
824
827
  ) {
825
-
826
828
  if let item = item {
827
829
  DispatchQueue.main.async {
828
830
  UIApplication.shared.beginReceivingRemoteControlEvents();
@@ -946,9 +948,8 @@ public class RNTrackPlayer: NSObject, AudioSessionControllerDelegate {
946
948
  public func setEqualizerEnabled(enabled: Bool, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
947
949
  if (rejectWhenNotInitialized(reject: reject)) { return }
948
950
 
949
- // Toggle the enabled flag
950
- // Note: For the equalizer to work, the tap must be attached to the player first
951
951
  equalizerTap.isEnabled = enabled
952
+
952
953
  resolve(NSNull())
953
954
  }
954
955
 
@@ -1015,6 +1016,57 @@ public class RNTrackPlayer: NSObject, AudioSessionControllerDelegate {
1015
1016
  equalizerTap.resetGains()
1016
1017
  resolve(NSNull())
1017
1018
  }
1019
+
1020
+ // MARK: - Audio Effects
1021
+
1022
+ @objc(setBassBoostEnabled:resolver:rejecter:)
1023
+ public func setBassBoostEnabled(enabled: Bool, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
1024
+ if (rejectWhenNotInitialized(reject: reject)) { return }
1025
+ equalizerTap.isBassBoostEnabled = enabled
1026
+ resolve(NSNull())
1027
+ }
1028
+
1029
+ @objc(setLoudnessEnabled:resolver:rejecter:)
1030
+ public func setLoudnessEnabled(enabled: Bool, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
1031
+ if (rejectWhenNotInitialized(reject: reject)) { return }
1032
+ equalizerTap.isLoudnessEnabled = enabled
1033
+ resolve(NSNull())
1034
+ }
1035
+
1036
+ @objc(setVirtualizerEnabled:resolver:rejecter:)
1037
+ public func setVirtualizerEnabled(enabled: Bool, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
1038
+ if (rejectWhenNotInitialized(reject: reject)) { return }
1039
+ equalizerTap.isVirtualizerEnabled = enabled
1040
+ resolve(NSNull())
1041
+ }
1042
+
1043
+ @objc(setBassBoostLevel:resolver:rejecter:)
1044
+ public func setBassBoostLevel(level: Float, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
1045
+ if (rejectWhenNotInitialized(reject: reject)) { return }
1046
+ equalizerTap.updateBassBoostLevel(level)
1047
+ resolve(NSNull())
1048
+ }
1049
+
1050
+ @objc(setLoudnessLevel:resolver:rejecter:)
1051
+ public func setLoudnessLevel(level: Float, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
1052
+ if (rejectWhenNotInitialized(reject: reject)) { return }
1053
+ equalizerTap.updateLoudnessLevel(level)
1054
+ resolve(NSNull())
1055
+ }
1056
+
1057
+ @objc(setVirtualizerLevel:resolver:rejecter:)
1058
+ public func setVirtualizerLevel(level: Float, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
1059
+ if (rejectWhenNotInitialized(reject: reject)) { return }
1060
+ equalizerTap.updateVirtualizerLevel(level)
1061
+ resolve(NSNull())
1062
+ }
1063
+
1064
+ @objc(setBalance:resolver:rejecter:)
1065
+ public func setBalance(balance: Float, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
1066
+ if (rejectWhenNotInitialized(reject: reject)) { return }
1067
+ equalizerTap.balance = max(-1, min(1, balance))
1068
+ resolve(NSNull())
1069
+ }
1018
1070
  }
1019
1071
 
1020
1072
  extension RNTrackPlayer {
@@ -284,4 +284,34 @@ RCT_EXPORT_MODULE()
284
284
  [trackPlayer resetEqualizer:resolve rejecter:reject];
285
285
  }
286
286
 
287
+ // Audio Effects
288
+
289
+ - (void)setBassBoostEnabled:(BOOL)enabled resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject {
290
+ [trackPlayer setBassBoostEnabled:enabled resolver:resolve rejecter:reject];
291
+ }
292
+
293
+ - (void)setLoudnessEnabled:(BOOL)enabled resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject {
294
+ [trackPlayer setLoudnessEnabled:enabled resolver:resolve rejecter:reject];
295
+ }
296
+
297
+ - (void)setVirtualizerEnabled:(BOOL)enabled resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject {
298
+ [trackPlayer setVirtualizerEnabled:enabled resolver:resolve rejecter:reject];
299
+ }
300
+
301
+ - (void)setBassBoostLevel:(double)level resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject {
302
+ [trackPlayer setBassBoostLevel:(float)level resolver:resolve rejecter:reject];
303
+ }
304
+
305
+ - (void)setLoudnessLevel:(double)level resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject {
306
+ [trackPlayer setLoudnessLevel:(float)level resolver:resolve rejecter:reject];
307
+ }
308
+
309
+ - (void)setVirtualizerLevel:(double)level resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject {
310
+ [trackPlayer setVirtualizerLevel:(float)level resolver:resolve rejecter:reject];
311
+ }
312
+
313
+ - (void)setBalance:(double)balance resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject {
314
+ [trackPlayer setBalance:(float)balance resolver:resolve rejecter:reject];
315
+ }
316
+
287
317
  @end