@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.
@@ -389,6 +389,49 @@ class MusicModule(reactContext: ReactApplicationContext) : NativeTrackPlayerSpec
389
389
  callback.resolve(null)
390
390
  }
391
391
 
392
+ // Audio Effects (BassBoost, Loudness, Virtualizer)
393
+
394
+ override fun setBassBoostEnabled(enabled: Boolean, callback: Promise) = launchInScope {
395
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
396
+ musicService.setBassBoostEnabled(enabled)
397
+ callback.resolve(null)
398
+ }
399
+
400
+ override fun setBassBoostLevel(level: Double, callback: Promise) = launchInScope {
401
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
402
+ musicService.setBassBoostLevel(level.toFloat())
403
+ callback.resolve(null)
404
+ }
405
+
406
+ override fun setLoudnessEnabled(enabled: Boolean, callback: Promise) = launchInScope {
407
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
408
+ musicService.setLoudnessEnabled(enabled)
409
+ callback.resolve(null)
410
+ }
411
+
412
+ override fun setLoudnessLevel(level: Double, callback: Promise) = launchInScope {
413
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
414
+ musicService.setLoudnessLevel(level.toFloat())
415
+ callback.resolve(null)
416
+ }
417
+
418
+ override fun setVirtualizerEnabled(enabled: Boolean, callback: Promise) = launchInScope {
419
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
420
+ musicService.setVirtualizerEnabled(enabled)
421
+ callback.resolve(null)
422
+ }
423
+
424
+ override fun setVirtualizerLevel(level: Double, callback: Promise) = launchInScope {
425
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
426
+ musicService.setVirtualizerLevel(level.toFloat())
427
+ callback.resolve(null)
428
+ }
429
+
430
+ override fun setBalance(balance: Double, callback: Promise) = launchInScope {
431
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
432
+ musicService.setBalance(balance.toFloat())
433
+ callback.resolve(null)
434
+ }
392
435
 
393
436
  override fun add(data: ReadableArray?, insertBeforeIndex: Double, callback: Promise) = launchInScope {
394
437
  if (verifyServiceBoundOrReject(callback)) return@launchInScope
@@ -201,6 +201,36 @@ class MusicService : HeadlessJsMediaService() {
201
201
  player.resetEqualizer()
202
202
  }
203
203
 
204
+ // Audio Effects (BassBoost, Loudness, Virtualizer)
205
+
206
+ fun setBassBoostEnabled(enabled: Boolean) {
207
+ player.setBassBoostEnabled(enabled)
208
+ }
209
+
210
+ fun setBassBoostLevel(level: Float) {
211
+ player.setBassBoostLevel(level)
212
+ }
213
+
214
+ fun setLoudnessEnabled(enabled: Boolean) {
215
+ player.setLoudnessEnabled(enabled)
216
+ }
217
+
218
+ fun setLoudnessLevel(level: Float) {
219
+ player.setLoudnessLevel(level)
220
+ }
221
+
222
+ fun setVirtualizerEnabled(enabled: Boolean) {
223
+ player.setVirtualizerEnabled(enabled)
224
+ }
225
+
226
+ fun setVirtualizerLevel(level: Float) {
227
+ player.setVirtualizerLevel(level)
228
+ }
229
+
230
+ fun setBalance(balance: Float) {
231
+ player.setBalance(balance)
232
+ }
233
+
204
234
  fun crossFadePrepare(previous: Boolean = false, seekTo: Double = 0.0) {
205
235
  player.crossFadePrepare(previous, seekTo)
206
236
  }
@@ -2,8 +2,10 @@
2
2
 
3
3
  import android.content.Context
4
4
  import android.media.AudioManager
5
+ import android.media.audiofx.BassBoost
5
6
  import android.media.audiofx.Equalizer
6
7
  import android.media.audiofx.LoudnessEnhancer
8
+ import android.media.audiofx.Virtualizer
7
9
  import androidx.annotation.CallSuper
8
10
  import androidx.annotation.OptIn
9
11
  import androidx.media3.common.AudioAttributes
@@ -38,6 +40,7 @@ import com.lovegaoshi.kotlinaudio.player.components.Cache
38
40
  import com.lovegaoshi.kotlinaudio.player.components.FocusManager
39
41
  import com.lovegaoshi.kotlinaudio.player.components.MediaFactory
40
42
  import com.lovegaoshi.kotlinaudio.player.components.setupBuffer
43
+ import com.lovegaoshi.kotlinaudio.processors.BalanceAudioProcessor
41
44
  import com.lovegaoshi.kotlinaudio.processors.FFTEmitter
42
45
  import kotlinx.coroutines.Deferred
43
46
  import kotlinx.coroutines.MainScope
@@ -59,6 +62,8 @@ abstract class AudioPlayer internal constructor(
59
62
  private var exoPlayer2: ExoPlayer? = null
60
63
  private var loudnessEnhancers = ArrayList<LoudnessEnhancer>()
61
64
  private var equalizers = ArrayList<Equalizer>()
65
+ private var bassBoosts = ArrayList<BassBoost>()
66
+ private var virtualizers = ArrayList<Virtualizer>()
62
67
  private var currentExoPlayer = true
63
68
 
64
69
  var exoPlayer: ExoPlayer
@@ -70,6 +75,7 @@ abstract class AudioPlayer internal constructor(
70
75
  private val focusListener = APMFocusListener()
71
76
  private val focusManager = FocusManager(context, listener=focusListener, options=options)
72
77
  var fftEmitter: (DoubleArray) -> Unit = { v -> Timber.tag("APMFFT").d("FFT emitted $v") }
78
+ private val balanceProcessor = BalanceAudioProcessor()
73
79
 
74
80
  var alwaysPauseOnInterruption: Boolean
75
81
  get() = focusManager.alwaysPauseOnInterruption
@@ -209,7 +215,9 @@ abstract class AudioPlayer internal constructor(
209
215
  }
210
216
  }
211
217
 
212
- }) else DefaultRenderersFactory(context)
218
+ }, arrayOf(balanceProcessor)) else APMRenderersFactory(
219
+ context, 0, null, arrayOf(balanceProcessor)
220
+ )
213
221
  renderer.setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER)
214
222
  val mPlayer = ExoPlayer
215
223
  .Builder(context)
@@ -407,36 +415,102 @@ abstract class AudioPlayer internal constructor(
407
415
  }
408
416
  }
409
417
 
418
+ // Bass Boost API
419
+
420
+ fun setBassBoostEnabled(enabled: Boolean) {
421
+ bassBoosts.forEach { it.enabled = enabled }
422
+ }
423
+
424
+ 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
+ }
431
+ }
432
+
433
+ // Loudness Enhancer API (enable/disable + level)
434
+
435
+ fun setLoudnessEnabled(enabled: Boolean) {
436
+ loudnessEnhancers.forEach { it.enabled = enabled }
437
+ }
438
+
439
+ 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
+ }
446
+ }
447
+
448
+ // Virtualizer API
449
+
450
+ fun setVirtualizerEnabled(enabled: Boolean) {
451
+ virtualizers.forEach { it.enabled = enabled }
452
+ }
453
+
454
+ 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
+ }
461
+ }
462
+
463
+ // Balance API
464
+
465
+ fun setBalance(balance: Float) {
466
+ balanceProcessor.setBalance(balance)
467
+ }
468
+
469
+ fun getBalance(): Float = balanceProcessor.getBalance()
470
+
410
471
  /**
411
472
  * Get preset names for iOS compatibility (custom presets mapped to Android system presets)
412
473
  */
413
474
  fun getEqualizerPresetNames(): List<String> {
414
- // Return iOS-compatible preset names
475
+ // Must match iOS EqualizerAudioTap.Preset order
415
476
  return listOf(
416
- "Flat", "Rock", "Pop", "Jazz", "Classical",
417
- "Hip Hop", "Electronic", "Acoustic", "Bass Boost",
418
- "Treble Boost", "Vocal", "Loudness"
477
+ "eqAcoustic", "eqBassBooster", "eqBassReducer", "eqClassical",
478
+ "eqDance", "eqDeep", "eqElectronic", "eqFlat",
479
+ "eqHipHop", "eqJazz", "eqLatin", "eqLoudness",
480
+ "eqLounge", "eqPiano", "eqPop", "eqRnb",
481
+ "eqRock", "eqSmallSpeakers", "eqSpokenWord",
482
+ "eqTrebleBooster", "eqTrebleReducer", "eqVocalBooster"
419
483
  )
420
484
  }
421
485
 
422
486
  /**
423
- * Apply a preset by index (iOS-compatible)
424
- * Maps iOS preset index to gain values
487
+ * Apply a preset by index (matches iOS preset order)
488
+ * 8 bands: 60, 150, 400, 1K, 2.5K, 6K, 12K, 16K
425
489
  */
426
490
  fun applyEqualizerPreset(presetIndex: Int) {
427
491
  val presets = listOf(
428
- listOf(0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f), // Flat
429
- listOf(5f, 4f, 3f, 1f, -1f, 0f, 2f, 3f, 4f, 4f), // Rock
430
- listOf(-1f, 1f, 3f, 4f, 3f, 1f, 0f, 1f, 2f, 2f), // Pop
431
- listOf(3f, 2f, 1f, 2f, -1f, -1f, 0f, 1f, 2f, 3f), // Jazz
432
- listOf(4f, 3f, 2f, 1f, -1f, -1f, 0f, 2f, 3f, 4f), // Classical
433
- listOf(5f, 5f, 3f, 1f, -1f, 0f, 1f, 0f, 2f, 3f), // Hip Hop
434
- listOf(4f, 4f, 2f, 0f, -2f, -1f, 0f, 2f, 4f, 4f), // Electronic
435
- listOf(3f, 2f, 1f, 1f, 0f, 0f, 1f, 2f, 2f, 2f), // Acoustic
436
- listOf(6f, 5f, 4f, 2f, 0f, 0f, 0f, 0f, 0f, 0f), // Bass Boost
437
- listOf(0f, 0f, 0f, 0f, 0f, 1f, 2f, 4f, 5f, 6f), // Treble Boost
438
- listOf(-2f, -1f, 0f, 2f, 4f, 4f, 3f, 1f, 0f, -1f), // Vocal
439
- listOf(5f, 4f, 2f, 0f, -2f, -2f, 0f, 2f, 4f, 5f) // Loudness
492
+ listOf( 3f, 2f, 1f, 0f, 1f, 2f, 2f, 2f), // Acoustic
493
+ listOf( 6f, 5f, 3f, 1f, 0f, 0f, 0f, 0f), // Bass Booster
494
+ listOf(-6f, -5f, -3f, -1f, 0f, 0f, 0f, 0f), // Bass Reducer
495
+ listOf( 4f, 2f, 0f, -1f, 0f, 2f, 3f, 3f), // Classical
496
+ listOf( 5f, 4f, 1f, 0f, 2f, 4f, 3f, 2f), // Dance
497
+ listOf( 5f, 4f, 2f, 1f, 0f, -1f, -2f, -3f), // Deep
498
+ listOf( 4f, 3f, 0f, -1f, 0f, 3f, 4f, 4f), // Electronic
499
+ listOf( 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f), // Flat
500
+ listOf( 5f, 4f, 2f, 0f, -1f, 1f, 2f, 3f), // Hip-Hop
501
+ listOf( 3f, 2f, 1f, 0f, -1f, 1f, 2f, 2f), // Jazz
502
+ listOf( 4f, 3f, 0f, -1f, 0f, 2f, 4f, 3f), // Latin
503
+ listOf( 5f, 3f, 0f, -2f, 0f, 2f, 4f, 4f), // Loudness
504
+ listOf(-1f, 1f, 2f, 1f, 0f, -1f, 1f, 1f), // Lounge
505
+ listOf( 1f, 0f, 1f, 2f, 3f, 2f, 2f, 1f), // Piano
506
+ listOf(-1f, 1f, 3f, 3f, 2f, 1f, 1f, 1f), // Pop
507
+ listOf( 5f, 4f, 2f, 0f, -1f, 1f, 2f, 2f), // R&B
508
+ listOf( 4f, 3f, 0f, -1f, 1f, 3f, 4f, 3f), // Rock
509
+ listOf( 5f, 4f, 3f, 1f, 0f, -1f, 2f, 3f), // Small Speakers
510
+ listOf(-2f, 0f, 1f, 3f, 4f, 3f, 1f, -1f), // Spoken Word
511
+ listOf( 0f, 0f, 0f, 0f, 1f, 3f, 5f, 6f), // Treble Booster
512
+ listOf( 0f, 0f, 0f, 0f, -1f, -3f, -5f, -6f), // Treble Reducer
513
+ listOf(-2f, -1f, 1f, 3f, 4f, 3f, 1f, 0f) // Vocal Booster
440
514
  )
441
515
  if (presetIndex >= 0 && presetIndex < presets.size) {
442
516
  setEqualizerBands(presets[presetIndex])
@@ -512,6 +586,8 @@ abstract class AudioPlayer internal constructor(
512
586
  }
513
587
  equalizers.forEach { e -> e.release() }
514
588
  loudnessEnhancers.forEach { e -> e.release() }
589
+ bassBoosts.forEach { e -> e.release() }
590
+ virtualizers.forEach { e -> e.release() }
515
591
  cache?.release()
516
592
  cache = null
517
593
  }
@@ -620,6 +696,22 @@ abstract class AudioPlayer internal constructor(
620
696
  } catch (e: RuntimeException) {
621
697
  Timber.tag("APMAudioFx").e("[AudioFx] failed to load equalizer. it's fine if in dev!")
622
698
  }
699
+
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
+ }
623
715
  }
624
716
  }
625
717
 
@@ -1,6 +1,7 @@
1
1
  package com.lovegaoshi.kotlinaudio.player.components
2
2
 
3
3
  import android.content.Context
4
+ import androidx.media3.common.audio.AudioProcessor
4
5
  import androidx.media3.common.util.UnstableApi
5
6
  import androidx.media3.exoplayer.DefaultRenderersFactory
6
7
  import androidx.media3.exoplayer.audio.AudioSink
@@ -13,7 +14,8 @@ import com.lovegaoshi.kotlinaudio.processors.TeeListener
13
14
  class APMRenderersFactory(
14
15
  context: Context,
15
16
  sampleRate: Int = 4096,
16
- emitter: FFTEmitter?
17
+ emitter: FFTEmitter?,
18
+ private val extraProcessors: Array<AudioProcessor> = emptyArray()
17
19
  ) : DefaultRenderersFactory(context) {
18
20
  val teeProcessor = TeeAudioProcessor(TeeListener(sampleRate, emitter))
19
21
 
@@ -26,8 +28,8 @@ class APMRenderersFactory(
26
28
  return DefaultAudioSink.Builder(context)
27
29
  .setEnableFloatOutput(enableFloatOutput)
28
30
  .setEnableAudioTrackPlaybackParams(enableAudioTrackPlaybackParams)
29
- .setAudioProcessors(arrayOf(teeProcessor))
31
+ .setAudioProcessors(arrayOf(*extraProcessors, teeProcessor))
30
32
  .build()
31
33
  }
32
34
 
33
- }
35
+ }
@@ -0,0 +1,62 @@
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
+
9
+ @UnstableApi
10
+ class BalanceAudioProcessor : BaseAudioProcessor() {
11
+ @Volatile
12
+ private var balance: Float = 0f
13
+
14
+ fun setBalance(value: Float) {
15
+ balance = value.coerceIn(-1f, 1f)
16
+ }
17
+
18
+ fun getBalance(): Float = balance
19
+
20
+ override fun onConfigure(inputAudioFormat: AudioProcessor.AudioFormat): AudioProcessor.AudioFormat {
21
+ if (inputAudioFormat.channelCount == 2 &&
22
+ (inputAudioFormat.encoding == C.ENCODING_PCM_16BIT || inputAudioFormat.encoding == C.ENCODING_PCM_FLOAT)
23
+ ) {
24
+ return inputAudioFormat
25
+ }
26
+ return AudioProcessor.AudioFormat.NOT_SET
27
+ }
28
+
29
+ override fun queueInput(inputBuffer: ByteBuffer) {
30
+ val bal = balance
31
+ if (bal == 0f) {
32
+ replaceOutputBuffer(inputBuffer.remaining()).put(inputBuffer).flip()
33
+ return
34
+ }
35
+
36
+ val leftGain = if (bal <= 0f) 1f else 1f - bal
37
+ val rightGain = if (bal >= 0f) 1f else 1f + bal
38
+
39
+ val size = inputBuffer.remaining()
40
+ val output = replaceOutputBuffer(size)
41
+
42
+ when (inputAudioFormat.encoding) {
43
+ C.ENCODING_PCM_16BIT -> {
44
+ while (inputBuffer.hasRemaining()) {
45
+ val left = inputBuffer.short
46
+ val right = inputBuffer.short
47
+ output.putShort((left * leftGain).toInt().coerceIn(Short.MIN_VALUE.toInt(), Short.MAX_VALUE.toInt()).toShort())
48
+ output.putShort((right * rightGain).toInt().coerceIn(Short.MIN_VALUE.toInt(), Short.MAX_VALUE.toInt()).toShort())
49
+ }
50
+ }
51
+ C.ENCODING_PCM_FLOAT -> {
52
+ while (inputBuffer.hasRemaining()) {
53
+ val left = inputBuffer.float
54
+ val right = inputBuffer.float
55
+ output.putFloat(left * leftGain)
56
+ output.putFloat(right * rightGain)
57
+ }
58
+ }
59
+ }
60
+ output.flip()
61
+ }
62
+ }
@@ -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
@@ -215,8 +215,8 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
215
215
  // the loadNowPlayingMetaValues call straight after:
216
216
  nowPlayingInfoController.setWithoutUpdate(keyValues: [
217
217
  MediaItemProperty.duration(item.getDuration()),
218
- NowPlayingInfoProperty.playbackRate(nil),
219
- NowPlayingInfoProperty.elapsedPlaybackTime(nil)
218
+ NowPlayingInfoProperty.playbackRate(0.0),
219
+ NowPlayingInfoProperty.elapsedPlaybackTime(0.0)
220
220
  ])
221
221
  loadNowPlayingMetaValues()
222
222
  }
@@ -398,7 +398,9 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
398
398
  }
399
399
 
400
400
  switch state {
401
- case .ready, .loading, .playing, .paused:
401
+ case .ready, .playing, .paused:
402
+ // Only update on ready/playing/paused, not on loading (removed .loading)
403
+ // This prevents showing incorrect progress during item transitions
402
404
  if (automaticallyUpdateNowPlayingInfo) {
403
405
  updateNowPlayingPlaybackValues()
404
406
  }
@@ -41,55 +41,58 @@ open class AudioTap {
41
41
  extension AVPlayerWrapper {
42
42
  internal func attachTap(_ tap: AudioTap?, to item: AVPlayerItem) {
43
43
  guard let tap else { return }
44
- guard let track = item.asset.tracks(withMediaType: .audio).first else {
44
+
45
+ // Try sync first (works for local/progressive files)
46
+ if let track = item.asset.tracks(withMediaType: .audio).first {
47
+ applyTap(tap, to: item, track: track)
45
48
  return
46
49
  }
47
-
50
+
51
+ // Async fallback for HLS streams where tracks aren't immediately available
52
+ Task {
53
+ guard let tracks = try? await item.asset.loadTracks(withMediaType: .audio),
54
+ let track = tracks.first else { return }
55
+ await MainActor.run {
56
+ self.applyTap(tap, to: item, track: track)
57
+ }
58
+ }
59
+ }
60
+
61
+ private func applyTap(_ tap: AudioTap, to item: AVPlayerItem, track: AVAssetTrack) {
48
62
  let audioMix = AVMutableAudioMix()
49
63
  let params = AVMutableAudioMixInputParameters(track: track)
50
-
51
- // we need to retain this pointer so it doesn't disappear out from under us.
52
- // we'll then let it go after we finalize. If the tap changed upstream, we
53
- // aren't going to pick up the new one until after this player item goes away.
64
+
54
65
  let client = UnsafeMutableRawPointer(Unmanaged.passRetained(tap).toOpaque())
55
-
66
+
56
67
  var callbacks = MTAudioProcessingTapCallbacks(version: kMTAudioProcessingTapCallbacksVersion_0, clientInfo: client)
57
68
  { tapRef, clientInfo, tapStorageOut in
58
- // initial tap setup
59
69
  guard let clientInfo else { return }
60
70
  tapStorageOut.pointee = clientInfo
61
71
  let audioTap = Unmanaged<AudioTap>.fromOpaque(clientInfo).takeUnretainedValue()
62
72
  audioTap.initialize()
63
73
  } finalize: { tapRef in
64
- // clean up
65
74
  let audioTap = Unmanaged<AudioTap>.fromOpaque(MTAudioProcessingTapGetStorage(tapRef)).takeUnretainedValue()
66
75
  audioTap.finalize()
67
- // we're done, we can let go of the pointer we retained.
68
76
  Unmanaged.passUnretained(audioTap).release()
69
77
  } prepare: { tapRef, maxFrames, processingFormat in
70
- // allocate memory for sound processing
71
78
  let audioTap = Unmanaged<AudioTap>.fromOpaque(MTAudioProcessingTapGetStorage(tapRef)).takeUnretainedValue()
72
79
  audioTap.prepare(description: processingFormat.pointee)
73
80
  } unprepare: { tapRef in
74
- // deallocate memory for sound processing
75
81
  let audioTap = Unmanaged<AudioTap>.fromOpaque(MTAudioProcessingTapGetStorage(tapRef)).takeUnretainedValue()
76
82
  audioTap.unprepare()
77
83
  } process: { tapRef, numberFrames, flags, bufferListInOut, numberFramesOut, flagsOut in
78
84
  guard noErr == MTAudioProcessingTapGetSourceAudio(tapRef, numberFrames, bufferListInOut, flagsOut, nil, numberFramesOut) else {
79
85
  return
80
86
  }
81
-
82
- // process sound data
83
87
  let audioTap = Unmanaged<AudioTap>.fromOpaque(MTAudioProcessingTapGetStorage(tapRef)).takeUnretainedValue()
84
88
  audioTap.process(numberOfFrames: numberFrames, buffer: UnsafeMutableAudioBufferListPointer(bufferListInOut))
85
89
  }
86
-
90
+
87
91
  var tapRef: MTAudioProcessingTap?
88
92
  let error = MTAudioProcessingTapCreate(kCFAllocatorDefault, &callbacks, kMTAudioProcessingTapCreationFlag_PreEffects, &tapRef)
89
- assert(error == noErr)
90
-
93
+ if error != noErr { return }
94
+
91
95
  params.audioTapProcessor = tapRef
92
-
93
96
  audioMix.inputParameters = [params]
94
97
  item.audioMix = audioMix
95
98
  }