@javascriptcommon/react-native-track-player 4.1.24 → 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/lovegaoshi/kotlinaudio/models/AudioItem.kt +3 -2
- package/android/src/main/java/com/lovegaoshi/kotlinaudio/player/AudioPlayer.kt +37 -144
- 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 +5 -1
- 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/package.json +1 -1
|
@@ -62,7 +62,7 @@ class Track
|
|
|
62
62
|
headers!![header] = httpHeaders.getString(header)!!
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
|
-
notPlayable = bundle.getBoolean("notPlayable", false)
|
|
65
|
+
notPlayable = bundle.getBoolean("notPlayable", false) || uri == null || uri.toString().isBlank()
|
|
66
66
|
setMetadata(context, bundle, ratingType)
|
|
67
67
|
queueId = System.currentTimeMillis()
|
|
68
68
|
originalItem = bundle
|
|
@@ -8,6 +8,8 @@ import androidx.media3.common.MediaItem
|
|
|
8
8
|
import androidx.media3.common.MediaMetadata
|
|
9
9
|
import com.lovegaoshi.kotlinaudio.utils.getEmbeddedBitmapArray
|
|
10
10
|
import com.lovegaoshi.kotlinaudio.utils.saveMediaCoverToPng
|
|
11
|
+
import androidx.media3.datasource.RawResourceDataSource
|
|
12
|
+
import com.doublesymmetry.trackplayer.R
|
|
11
13
|
import java.util.UUID
|
|
12
14
|
|
|
13
15
|
|
|
@@ -83,8 +85,7 @@ fun audioItem2MediaItem(audioItem: AudioItem, context: Context? = null): MediaIt
|
|
|
83
85
|
|
|
84
86
|
return MediaItem.Builder()
|
|
85
87
|
.setMediaId(audioItem.mediaId ?: UUID.randomUUID().toString())
|
|
86
|
-
|
|
87
|
-
.setUri(audioItem.audioUrl)
|
|
88
|
+
.setUri(if (hasValidUrl && !isNotPlayable) audioItem.audioUrl else RawResourceDataSource.buildRawResourceUri(R.raw.silent_5_seconds).toString())
|
|
88
89
|
.setMediaMetadata(
|
|
89
90
|
MediaMetadata.Builder()
|
|
90
91
|
.setTitle(audioItem.title)
|
|
@@ -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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
339
|
+
equalizerProcessor.isBassBoostEnabled = enabled
|
|
422
340
|
}
|
|
423
341
|
|
|
424
342
|
fun setBassBoostLevel(level: Float) {
|
|
425
|
-
|
|
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 (
|
|
346
|
+
// Loudness Enhancer API (software DSP — matches iOS low+high shelf)
|
|
434
347
|
|
|
435
348
|
fun setLoudnessEnabled(enabled: Boolean) {
|
|
436
|
-
|
|
349
|
+
equalizerProcessor.isLoudnessEnabled = enabled
|
|
437
350
|
}
|
|
438
351
|
|
|
439
352
|
fun setLoudnessLevel(level: Float) {
|
|
440
|
-
|
|
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
|
-
|
|
359
|
+
equalizerProcessor.isVirtualizerEnabled = enabled
|
|
452
360
|
}
|
|
453
361
|
|
|
454
362
|
fun setVirtualizerLevel(level: Float) {
|
|
455
|
-
|
|
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
|
|
516
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
701
|
-
|
|
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
|
|
package/android/src/main/java/com/lovegaoshi/kotlinaudio/player/components/APMRenderersFactory.kt
CHANGED
|
@@ -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 =
|
|
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(
|
|
33
|
+
.setAudioProcessors(processors)
|
|
32
34
|
.build()
|
|
33
35
|
}
|
|
34
36
|
|
package/android/src/main/java/com/lovegaoshi/kotlinaudio/processors/BalanceAudioProcessor.kt
CHANGED
|
@@ -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())
|
|
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
|
|
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
|
package/package.json
CHANGED