@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.
- package/android/src/main/java/com/doublesymmetry/trackplayer/model/Track.kt +1 -1
- 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/models/AudioItem.kt +3 -2
- package/android/src/main/java/com/lovegaoshi/kotlinaudio/player/AudioPlayer.kt +99 -114
- package/android/src/main/java/com/lovegaoshi/kotlinaudio/player/components/APMRenderersFactory.kt +9 -5
- package/android/src/main/java/com/lovegaoshi/kotlinaudio/processors/BalanceAudioProcessor.kt +66 -0
- package/android/src/main/java/com/lovegaoshi/kotlinaudio/processors/EqualizerAudioProcessor.kt +517 -0
- package/android/src/main/res/raw/silent_5_seconds.mp3 +0 -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
package/android/src/main/java/com/lovegaoshi/kotlinaudio/processors/EqualizerAudioProcessor.kt
ADDED
|
@@ -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
|
+
}
|
|
Binary file
|
|
@@ -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
|