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

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,10 +2,8 @@
2
2
 
3
3
  import android.content.Context
4
4
  import android.media.AudioManager
5
- import android.media.audiofx.BassBoost
6
5
  import android.media.audiofx.Equalizer
7
6
  import android.media.audiofx.LoudnessEnhancer
8
- import android.media.audiofx.Virtualizer
9
7
  import androidx.annotation.CallSuper
10
8
  import androidx.annotation.OptIn
11
9
  import androidx.media3.common.AudioAttributes
@@ -41,6 +39,7 @@ import com.lovegaoshi.kotlinaudio.player.components.FocusManager
41
39
  import com.lovegaoshi.kotlinaudio.player.components.MediaFactory
42
40
  import com.lovegaoshi.kotlinaudio.player.components.setupBuffer
43
41
  import com.lovegaoshi.kotlinaudio.processors.BalanceAudioProcessor
42
+ import com.lovegaoshi.kotlinaudio.processors.EqualizerAudioProcessor
44
43
  import com.lovegaoshi.kotlinaudio.processors.FFTEmitter
45
44
  import kotlinx.coroutines.Deferred
46
45
  import kotlinx.coroutines.MainScope
@@ -62,8 +61,6 @@ abstract class AudioPlayer internal constructor(
62
61
  private var exoPlayer2: ExoPlayer? = null
63
62
  private var loudnessEnhancers = ArrayList<LoudnessEnhancer>()
64
63
  private var equalizers = ArrayList<Equalizer>()
65
- private var bassBoosts = ArrayList<BassBoost>()
66
- private var virtualizers = ArrayList<Virtualizer>()
67
64
  private var currentExoPlayer = true
68
65
 
69
66
  var exoPlayer: ExoPlayer
@@ -76,6 +73,7 @@ abstract class AudioPlayer internal constructor(
76
73
  private val focusManager = FocusManager(context, listener=focusListener, options=options)
77
74
  var fftEmitter: (DoubleArray) -> Unit = { v -> Timber.tag("APMFFT").d("FFT emitted $v") }
78
75
  private val balanceProcessor = BalanceAudioProcessor()
76
+ private val equalizerProcessor = EqualizerAudioProcessor()
79
77
 
80
78
  var alwaysPauseOnInterruption: Boolean
81
79
  get() = focusManager.alwaysPauseOnInterruption
@@ -215,8 +213,8 @@ abstract class AudioPlayer internal constructor(
215
213
  }
216
214
  }
217
215
 
218
- }, arrayOf(balanceProcessor)) else APMRenderersFactory(
219
- context, 0, null, arrayOf(balanceProcessor)
216
+ }, arrayOf(equalizerProcessor, balanceProcessor)) else APMRenderersFactory(
217
+ context, 0, null, arrayOf(equalizerProcessor, balanceProcessor)
220
218
  )
221
219
  renderer.setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER)
222
220
  val mPlayer = ExoPlayer
@@ -303,161 +301,66 @@ abstract class AudioPlayer internal constructor(
303
301
  .map { i -> equalizers[0].getPresetName(i.toShort()) }
304
302
  }
305
303
 
306
- // 10-band Equalizer API (cross-platform compatible)
304
+ // 8-band Software Equalizer API (cross-platform, biquad with coefficient smoothing)
307
305
 
308
- /**
309
- * Enable or disable the equalizer
310
- */
311
306
  fun setEqualizerEnabled(enabled: Boolean) {
312
- equalizers.forEach { equalizer ->
313
- equalizer.enabled = enabled
314
- }
307
+ equalizerProcessor.isEnabled = enabled
315
308
  }
316
309
 
317
- /**
318
- * Check if the equalizer is enabled
319
- */
320
- fun getEqualizerEnabled(): Boolean {
321
- if (equalizers.isEmpty()) return false
322
- return equalizers[0].enabled
323
- }
310
+ fun getEqualizerEnabled(): Boolean = equalizerProcessor.isEnabled
324
311
 
325
- /**
326
- * Get the number of equalizer bands available
327
- */
328
- fun getEqualizerBandCount(): Int {
329
- if (equalizers.isEmpty()) return 0
330
- return equalizers[0].numberOfBands.toInt()
331
- }
312
+ fun getEqualizerBandCount(): Int = EqualizerAudioProcessor.BAND_COUNT
332
313
 
333
- /**
334
- * Set the gain for a specific equalizer band
335
- * @param band Band index (0 to bandCount-1)
336
- * @param gainDB Gain in millibels (mB). 1 dB = 100 mB
337
- */
338
314
  fun setEqualizerBand(band: Int, gainDB: Float) {
339
- equalizers.forEach { equalizer ->
340
- if (band >= 0 && band < equalizer.numberOfBands) {
341
- // Convert dB to millibels (mB)
342
- val millibels = (gainDB * 100).toInt().toShort()
343
- val range = equalizer.bandLevelRange
344
- val clampedLevel = millibels.coerceIn(range[0], range[1])
345
- equalizer.setBandLevel(band.toShort(), clampedLevel)
346
- equalizer.enabled = true
347
- }
348
- }
315
+ equalizerProcessor.setGain(band, gainDB)
349
316
  }
350
317
 
351
- /**
352
- * Set gains for all equalizer bands at once
353
- * @param gainsDB Array of gain values in dB
354
- */
355
318
  fun setEqualizerBands(gainsDB: List<Float>) {
356
- equalizers.forEach { equalizer ->
357
- val bandCount = equalizer.numberOfBands.toInt()
358
- val range = equalizer.bandLevelRange
359
- for (i in 0 until minOf(gainsDB.size, bandCount)) {
360
- val millibels = (gainsDB[i] * 100).toInt().toShort()
361
- val clampedLevel = millibels.coerceIn(range[0], range[1])
362
- equalizer.setBandLevel(i.toShort(), clampedLevel)
363
- }
364
- equalizer.enabled = true
365
- }
319
+ equalizerProcessor.setAllGains(gainsDB)
366
320
  }
367
321
 
368
- /**
369
- * Get all current equalizer band gains
370
- * @return Array of gain values in dB
371
- */
372
322
  fun getEqualizerBands(): List<Float> {
373
- if (equalizers.isEmpty()) return emptyList()
374
- val equalizer = equalizers[0]
375
- val bandCount = equalizer.numberOfBands.toInt()
376
- return (0 until bandCount).map { band ->
377
- // Convert millibels to dB
378
- equalizer.getBandLevel(band.toShort()).toFloat() / 100f
379
- }
323
+ return equalizerProcessor.getAllGains().toList()
380
324
  }
381
325
 
382
- /**
383
- * Get the center frequencies for each equalizer band
384
- * @return Array of frequency values in Hz
385
- */
386
326
  fun getEqualizerFrequencies(): List<Int> {
387
- if (equalizers.isEmpty()) return emptyList()
388
- val equalizer = equalizers[0]
389
- val bandCount = equalizer.numberOfBands.toInt()
390
- return (0 until bandCount).map { band ->
391
- // getCenterFreq returns milliHz, convert to Hz
392
- (equalizer.getCenterFreq(band.toShort()) / 1000)
393
- }
327
+ return EqualizerAudioProcessor.FREQUENCIES.map { it.toInt() }
394
328
  }
395
329
 
396
- /**
397
- * Get the band level range in dB [min, max]
398
- */
399
- fun getEqualizerBandLevelRange(): List<Float> {
400
- if (equalizers.isEmpty()) return listOf(-12f, 12f)
401
- val range = equalizers[0].bandLevelRange
402
- // Convert millibels to dB
403
- return listOf(range[0].toFloat() / 100f, range[1].toFloat() / 100f)
404
- }
330
+ fun getEqualizerBandLevelRange(): List<Float> = listOf(-12f, 12f)
405
331
 
406
- /**
407
- * Reset all equalizer bands to 0 (flat response)
408
- */
409
332
  fun resetEqualizer() {
410
- equalizers.forEach { equalizer ->
411
- val bandCount = equalizer.numberOfBands.toInt()
412
- for (i in 0 until bandCount) {
413
- equalizer.setBandLevel(i.toShort(), 0)
414
- }
415
- }
333
+ equalizerProcessor.resetGains()
416
334
  }
417
335
 
418
- // Bass Boost API
336
+ // Bass Boost API (software DSP — matches iOS low shelf filter)
419
337
 
420
338
  fun setBassBoostEnabled(enabled: Boolean) {
421
- bassBoosts.forEach { it.enabled = enabled }
339
+ equalizerProcessor.isBassBoostEnabled = enabled
422
340
  }
423
341
 
424
342
  fun setBassBoostLevel(level: Float) {
425
- // level 0.0-1.0 → strength 250-1000 (always boosts, never zero)
426
- val strength = (250 + level * 750).toInt().coerceIn(250, 1000).toShort()
427
- bassBoosts.forEach {
428
- it.setStrength(strength)
429
- it.enabled = true
430
- }
343
+ equalizerProcessor.updateBassBoostLevel(level)
431
344
  }
432
345
 
433
- // Loudness Enhancer API (enable/disable + level)
346
+ // Loudness Enhancer API (software DSP — matches iOS low+high shelf)
434
347
 
435
348
  fun setLoudnessEnabled(enabled: Boolean) {
436
- loudnessEnhancers.forEach { it.enabled = enabled }
349
+ equalizerProcessor.isLoudnessEnabled = enabled
437
350
  }
438
351
 
439
352
  fun setLoudnessLevel(level: Float) {
440
- // level 0.0-1.0 → target gain 100-1500 mB (1-15 dB, always enhances)
441
- val gainMb = (100 + level * 1400).toInt().coerceIn(100, 1500)
442
- loudnessEnhancers.forEach {
443
- it.setTargetGain(gainMb)
444
- it.enabled = true
445
- }
353
+ equalizerProcessor.updateLoudnessLevel(level)
446
354
  }
447
355
 
448
- // Virtualizer API
356
+ // Virtualizer API (software DSP — matches iOS all-pass stereo widener)
449
357
 
450
358
  fun setVirtualizerEnabled(enabled: Boolean) {
451
- virtualizers.forEach { it.enabled = enabled }
359
+ equalizerProcessor.isVirtualizerEnabled = enabled
452
360
  }
453
361
 
454
362
  fun setVirtualizerLevel(level: Float) {
455
- // level 0.0-1.0 → strength 200-1000 (always adds spatial width)
456
- val strength = (200 + level * 800).toInt().coerceIn(200, 1000).toShort()
457
- virtualizers.forEach {
458
- it.setStrength(strength)
459
- it.enabled = true
460
- }
363
+ equalizerProcessor.updateVirtualizerLevel(level)
461
364
  }
462
365
 
463
366
  // Balance API
@@ -484,8 +387,8 @@ abstract class AudioPlayer internal constructor(
484
387
  }
485
388
 
486
389
  /**
487
- * Apply a preset by index (matches iOS preset order)
488
- * 8 bands: 60, 150, 400, 1K, 2.5K, 6K, 12K, 16K
390
+ * Apply a preset by index (matches iOS preset order).
391
+ * 8 bands: 60, 150, 400, 1K, 2.5K, 6K, 12K, 16K — same as software EQ.
489
392
  */
490
393
  fun applyEqualizerPreset(presetIndex: Int) {
491
394
  val presets = listOf(
@@ -512,9 +415,8 @@ abstract class AudioPlayer internal constructor(
512
415
  listOf( 0f, 0f, 0f, 0f, -1f, -3f, -5f, -6f), // Treble Reducer
513
416
  listOf(-2f, -1f, 1f, 3f, 4f, 3f, 1f, 0f) // Vocal Booster
514
417
  )
515
- if (presetIndex >= 0 && presetIndex < presets.size) {
516
- setEqualizerBands(presets[presetIndex])
517
- }
418
+ if (presetIndex < 0 || presetIndex >= presets.size) return
419
+ setEqualizerBands(presets[presetIndex])
518
420
  }
519
421
 
520
422
  fun togglePlaying() {
@@ -586,8 +488,6 @@ abstract class AudioPlayer internal constructor(
586
488
  }
587
489
  equalizers.forEach { e -> e.release() }
588
490
  loudnessEnhancers.forEach { e -> e.release() }
589
- bassBoosts.forEach { e -> e.release() }
590
- virtualizers.forEach { e -> e.release() }
591
491
  cache?.release()
592
492
  cache = null
593
493
  }
@@ -681,7 +581,13 @@ abstract class AudioPlayer internal constructor(
681
581
  inner class AudioFxInitListener: AnalyticsListener {
682
582
  @OptIn(UnstableApi::class)
683
583
  override fun onAudioSessionIdChanged(eventTime: AnalyticsListener.EventTime, audioSessionId: Int) {
684
- // Try to add LoudnessEnhancer
584
+ // Release old native effects before creating new ones
585
+ loudnessEnhancers.forEach { try { it.release() } catch (_: Exception) {} }
586
+ loudnessEnhancers.clear()
587
+ equalizers.forEach { try { it.release() } catch (_: Exception) {} }
588
+ equalizers.clear()
589
+
590
+ // Native LoudnessEnhancer (only for setLoudnessEnhance legacy API)
685
591
  try {
686
592
  val enhancer = LoudnessEnhancer(audioSessionId)
687
593
  loudnessEnhancers.add(enhancer)
@@ -689,7 +595,7 @@ abstract class AudioPlayer internal constructor(
689
595
  Timber.tag("APMAudioFx").e("[AudioFx] failed to load loudnessEnhancer. it's fine if in dev!")
690
596
  }
691
597
 
692
- // Try to add Equalizer
598
+ // Native Equalizer (only for setEqualizerPreset legacy API)
693
599
  try {
694
600
  val equalizer = Equalizer(0, audioSessionId)
695
601
  equalizers.add(equalizer)
@@ -697,21 +603,8 @@ abstract class AudioPlayer internal constructor(
697
603
  Timber.tag("APMAudioFx").e("[AudioFx] failed to load equalizer. it's fine if in dev!")
698
604
  }
699
605
 
700
- // Try to add BassBoost
701
- try {
702
- val bassBoost = BassBoost(0, audioSessionId)
703
- bassBoosts.add(bassBoost)
704
- } catch (e: RuntimeException) {
705
- Timber.tag("APMAudioFx").e("[AudioFx] failed to load bassBoost. it's fine if in dev!")
706
- }
707
-
708
- // Try to add Virtualizer
709
- try {
710
- val virtualizer = Virtualizer(0, audioSessionId)
711
- virtualizers.add(virtualizer)
712
- } catch (e: RuntimeException) {
713
- Timber.tag("APMAudioFx").e("[AudioFx] failed to load virtualizer. it's fine if in dev!")
714
- }
606
+ // Bass boost, loudness, virtualizer, EQ bands — all handled by
607
+ // software EqualizerAudioProcessor (no native effects needed)
715
608
  }
716
609
  }
717
610
 
@@ -17,18 +17,20 @@ class APMRenderersFactory(
17
17
  emitter: FFTEmitter?,
18
18
  private val extraProcessors: Array<AudioProcessor> = emptyArray()
19
19
  ) : DefaultRenderersFactory(context) {
20
- val teeProcessor = TeeAudioProcessor(TeeListener(sampleRate, emitter))
21
-
20
+ val teeProcessor = if (sampleRate > 0 && emitter != null)
21
+ TeeAudioProcessor(TeeListener(sampleRate, emitter)) else null
22
22
 
23
23
  override fun buildAudioSink(
24
24
  context: Context,
25
25
  enableFloatOutput: Boolean,
26
26
  enableAudioTrackPlaybackParams: Boolean
27
27
  ): AudioSink? {
28
+ val processors = if (teeProcessor != null)
29
+ arrayOf(*extraProcessors, teeProcessor) else arrayOf(*extraProcessors)
28
30
  return DefaultAudioSink.Builder(context)
29
31
  .setEnableFloatOutput(enableFloatOutput)
30
32
  .setEnableAudioTrackPlaybackParams(enableAudioTrackPlaybackParams)
31
- .setAudioProcessors(arrayOf(*extraProcessors, teeProcessor))
33
+ .setAudioProcessors(processors)
32
34
  .build()
33
35
  }
34
36
 
@@ -29,7 +29,11 @@ class BalanceAudioProcessor : BaseAudioProcessor() {
29
29
  override fun queueInput(inputBuffer: ByteBuffer) {
30
30
  val bal = balance
31
31
  if (bal == 0f) {
32
- replaceOutputBuffer(inputBuffer.remaining()).put(inputBuffer).flip()
32
+ val output = replaceOutputBuffer(inputBuffer.remaining())
33
+ if (output !== inputBuffer) {
34
+ output.put(inputBuffer)
35
+ }
36
+ output.flip()
33
37
  return
34
38
  }
35
39
 
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@javascriptcommon/react-native-track-player",
3
- "version": "4.1.24",
3
+ "version": "4.1.26",
4
4
  "description": "A fully fledged audio module created for music apps",
5
5
  "main": "lib/src/index.js",
6
6
  "types": "lib/src/index.d.ts",