@javascriptcommon/react-native-track-player 4.1.23 → 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.
- package/android/src/main/java/com/doublesymmetry/trackplayer/module/MusicModule.kt +43 -0
- package/android/src/main/java/com/doublesymmetry/trackplayer/service/MusicService.kt +30 -0
- package/android/src/main/java/com/lovegaoshi/kotlinaudio/player/AudioPlayer.kt +111 -19
- package/android/src/main/java/com/lovegaoshi/kotlinaudio/player/components/APMRenderersFactory.kt +5 -3
- package/android/src/main/java/com/lovegaoshi/kotlinaudio/processors/BalanceAudioProcessor.kt +62 -0
- package/ios/RNTrackPlayer/RNTrackPlayer.swift +56 -4
- package/ios/RNTrackPlayer/TrackPlayer.mm +30 -0
- package/ios/SwiftAudioEx/Sources/SwiftAudioEx/AudioTap.swift +21 -18
- package/ios/SwiftAudioEx/Sources/SwiftAudioEx/EqualizerAudioTap.swift +565 -232
- package/lib/specs/NativeTrackPlayer.d.ts +7 -0
- package/lib/src/trackPlayer.d.ts +35 -0
- package/lib/src/trackPlayer.js +50 -0
- package/package.json +1 -1
- package/specs/NativeTrackPlayer.ts +9 -0
- package/src/trackPlayer.ts +58 -0
|
@@ -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
|
|
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
|
-
//
|
|
475
|
+
// Must match iOS EqualizerAudioTap.Preset order
|
|
415
476
|
return listOf(
|
|
416
|
-
"
|
|
417
|
-
"
|
|
418
|
-
"
|
|
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
|
|
424
|
-
*
|
|
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(
|
|
429
|
-
listOf(5f,
|
|
430
|
-
listOf(-
|
|
431
|
-
listOf(
|
|
432
|
-
listOf(
|
|
433
|
-
listOf(
|
|
434
|
-
listOf(
|
|
435
|
-
listOf(
|
|
436
|
-
listOf(
|
|
437
|
-
listOf(
|
|
438
|
-
listOf(
|
|
439
|
-
listOf(5f,
|
|
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
|
|
package/android/src/main/java/com/lovegaoshi/kotlinaudio/player/components/APMRenderersFactory.kt
CHANGED
|
@@ -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
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
}
|