@javascriptcommon/react-native-track-player 4.1.9 → 4.1.12

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.
@@ -111,4 +111,7 @@ dependencies {
111
111
 
112
112
  implementation 'androidx.test:rules:1.7.0'
113
113
  implementation 'jp.wasabeef.transformers:coil:1.0.6'
114
+
115
+ // FORK PATCH: Used by AutoConnectionDetector
116
+ implementation "androidx.car.app:app:1.7.0"
114
117
  }
@@ -325,6 +325,63 @@ class MusicModule(reactContext: ReactApplicationContext) : NativeTrackPlayerSpec
325
325
  callback.resolve(Arguments.fromList(musicService.getEqualizerPresets()))
326
326
  }
327
327
 
328
+ // Cross-platform Equalizer Band API
329
+
330
+ override fun setEqualizerEnabled(enabled: Boolean, callback: Promise) = launchInScope {
331
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
332
+ musicService.setEqualizerEnabled(enabled)
333
+ callback.resolve(null)
334
+ }
335
+
336
+ override fun getEqualizerEnabled(callback: Promise) = launchInScope {
337
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
338
+ callback.resolve(musicService.getEqualizerEnabled())
339
+ }
340
+
341
+ override fun setEqualizerBand(band: Double, gain: Double, callback: Promise) = launchInScope {
342
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
343
+ musicService.setEqualizerBand(band.toInt(), gain.toFloat())
344
+ callback.resolve(null)
345
+ }
346
+
347
+ override fun setEqualizerBands(gains: ReadableArray?, callback: Promise) = launchInScope {
348
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
349
+ val gainsList = mutableListOf<Float>()
350
+ gains?.let {
351
+ for (i in 0 until it.size()) {
352
+ gainsList.add(it.getDouble(i).toFloat())
353
+ }
354
+ }
355
+ musicService.setEqualizerBands(gainsList)
356
+ callback.resolve(null)
357
+ }
358
+
359
+ override fun getEqualizerBands(callback: Promise) = launchInScope {
360
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
361
+ callback.resolve(Arguments.fromList(musicService.getEqualizerBands()))
362
+ }
363
+
364
+ override fun getEqualizerFrequencies(callback: Promise) = launchInScope {
365
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
366
+ callback.resolve(Arguments.fromList(musicService.getEqualizerFrequencies()))
367
+ }
368
+
369
+ override fun applyEqualizerPreset(presetIndex: Double, callback: Promise) = launchInScope {
370
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
371
+ musicService.applyEqualizerPreset(presetIndex.toInt())
372
+ callback.resolve(null)
373
+ }
374
+
375
+ override fun getEqualizerPresetNames(callback: Promise) = launchInScope {
376
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
377
+ callback.resolve(Arguments.fromList(musicService.getEqualizerPresetNames()))
378
+ }
379
+
380
+ override fun resetEqualizer(callback: Promise) = launchInScope {
381
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
382
+ musicService.resetEqualizer()
383
+ callback.resolve(null)
384
+ }
328
385
 
329
386
  override fun setLoudnessEnhance(gain: Double, callback: Promise) = launchInScope {
330
387
  musicService.setLoudnessEnhance(gain.toInt())
@@ -156,6 +156,48 @@ class MusicService : HeadlessJsMediaService() {
156
156
  player.setLoudnessEnhance(gain)
157
157
  }
158
158
 
159
+ // Cross-platform Equalizer Band API
160
+
161
+ fun setEqualizerEnabled(enabled: Boolean) {
162
+ player.setEqualizerEnabled(enabled)
163
+ }
164
+
165
+ fun getEqualizerEnabled(): Boolean {
166
+ return player.getEqualizerEnabled()
167
+ }
168
+
169
+ fun setEqualizerBand(band: Int, gain: Float) {
170
+ player.setEqualizerBand(band, gain)
171
+ }
172
+
173
+ fun setEqualizerBands(gains: List<Float>) {
174
+ player.setEqualizerBands(gains)
175
+ }
176
+
177
+ fun getEqualizerBands(): List<Float> {
178
+ return player.getEqualizerBands()
179
+ }
180
+
181
+ fun getEqualizerFrequencies(): List<Int> {
182
+ return player.getEqualizerFrequencies()
183
+ }
184
+
185
+ fun getEqualizerBandLevelRange(): List<Float> {
186
+ return player.getEqualizerBandLevelRange()
187
+ }
188
+
189
+ fun applyEqualizerPreset(presetIndex: Int) {
190
+ player.applyEqualizerPreset(presetIndex)
191
+ }
192
+
193
+ fun getEqualizerPresetNames(): List<String> {
194
+ return player.getEqualizerPresetNames()
195
+ }
196
+
197
+ fun resetEqualizer() {
198
+ player.resetEqualizer()
199
+ }
200
+
159
201
  fun crossFadePrepare(previous: Boolean = false, seekTo: Double = 0.0) {
160
202
  player.crossFadePrepare(previous, seekTo)
161
203
  }
@@ -295,6 +295,154 @@ abstract class AudioPlayer internal constructor(
295
295
  .map { i -> equalizers[0].getPresetName(i.toShort()) }
296
296
  }
297
297
 
298
+ // 10-band Equalizer API (cross-platform compatible)
299
+
300
+ /**
301
+ * Enable or disable the equalizer
302
+ */
303
+ fun setEqualizerEnabled(enabled: Boolean) {
304
+ equalizers.forEach { equalizer ->
305
+ equalizer.enabled = enabled
306
+ }
307
+ }
308
+
309
+ /**
310
+ * Check if the equalizer is enabled
311
+ */
312
+ fun getEqualizerEnabled(): Boolean {
313
+ if (equalizers.isEmpty()) return false
314
+ return equalizers[0].enabled
315
+ }
316
+
317
+ /**
318
+ * Get the number of equalizer bands available
319
+ */
320
+ fun getEqualizerBandCount(): Int {
321
+ if (equalizers.isEmpty()) return 0
322
+ return equalizers[0].numberOfBands.toInt()
323
+ }
324
+
325
+ /**
326
+ * Set the gain for a specific equalizer band
327
+ * @param band Band index (0 to bandCount-1)
328
+ * @param gainDB Gain in millibels (mB). 1 dB = 100 mB
329
+ */
330
+ fun setEqualizerBand(band: Int, gainDB: Float) {
331
+ equalizers.forEach { equalizer ->
332
+ if (band >= 0 && band < equalizer.numberOfBands) {
333
+ // Convert dB to millibels (mB)
334
+ val millibels = (gainDB * 100).toInt().toShort()
335
+ val range = equalizer.bandLevelRange
336
+ val clampedLevel = millibels.coerceIn(range[0], range[1])
337
+ equalizer.setBandLevel(band.toShort(), clampedLevel)
338
+ equalizer.enabled = true
339
+ }
340
+ }
341
+ }
342
+
343
+ /**
344
+ * Set gains for all equalizer bands at once
345
+ * @param gainsDB Array of gain values in dB
346
+ */
347
+ fun setEqualizerBands(gainsDB: List<Float>) {
348
+ equalizers.forEach { equalizer ->
349
+ val bandCount = equalizer.numberOfBands.toInt()
350
+ val range = equalizer.bandLevelRange
351
+ for (i in 0 until minOf(gainsDB.size, bandCount)) {
352
+ val millibels = (gainsDB[i] * 100).toInt().toShort()
353
+ val clampedLevel = millibels.coerceIn(range[0], range[1])
354
+ equalizer.setBandLevel(i.toShort(), clampedLevel)
355
+ }
356
+ equalizer.enabled = true
357
+ }
358
+ }
359
+
360
+ /**
361
+ * Get all current equalizer band gains
362
+ * @return Array of gain values in dB
363
+ */
364
+ fun getEqualizerBands(): List<Float> {
365
+ if (equalizers.isEmpty()) return emptyList()
366
+ val equalizer = equalizers[0]
367
+ val bandCount = equalizer.numberOfBands.toInt()
368
+ return (0 until bandCount).map { band ->
369
+ // Convert millibels to dB
370
+ equalizer.getBandLevel(band.toShort()).toFloat() / 100f
371
+ }
372
+ }
373
+
374
+ /**
375
+ * Get the center frequencies for each equalizer band
376
+ * @return Array of frequency values in Hz
377
+ */
378
+ fun getEqualizerFrequencies(): List<Int> {
379
+ if (equalizers.isEmpty()) return emptyList()
380
+ val equalizer = equalizers[0]
381
+ val bandCount = equalizer.numberOfBands.toInt()
382
+ return (0 until bandCount).map { band ->
383
+ // getCenterFreq returns milliHz, convert to Hz
384
+ (equalizer.getCenterFreq(band.toShort()) / 1000)
385
+ }
386
+ }
387
+
388
+ /**
389
+ * Get the band level range in dB [min, max]
390
+ */
391
+ fun getEqualizerBandLevelRange(): List<Float> {
392
+ if (equalizers.isEmpty()) return listOf(-12f, 12f)
393
+ val range = equalizers[0].bandLevelRange
394
+ // Convert millibels to dB
395
+ return listOf(range[0].toFloat() / 100f, range[1].toFloat() / 100f)
396
+ }
397
+
398
+ /**
399
+ * Reset all equalizer bands to 0 (flat response)
400
+ */
401
+ fun resetEqualizer() {
402
+ equalizers.forEach { equalizer ->
403
+ val bandCount = equalizer.numberOfBands.toInt()
404
+ for (i in 0 until bandCount) {
405
+ equalizer.setBandLevel(i.toShort(), 0)
406
+ }
407
+ }
408
+ }
409
+
410
+ /**
411
+ * Get preset names for iOS compatibility (custom presets mapped to Android system presets)
412
+ */
413
+ fun getEqualizerPresetNames(): List<String> {
414
+ // Return iOS-compatible preset names
415
+ return listOf(
416
+ "Flat", "Rock", "Pop", "Jazz", "Classical",
417
+ "Hip Hop", "Electronic", "Acoustic", "Bass Boost",
418
+ "Treble Boost", "Vocal", "Loudness"
419
+ )
420
+ }
421
+
422
+ /**
423
+ * Apply a preset by index (iOS-compatible)
424
+ * Maps iOS preset index to gain values
425
+ */
426
+ fun applyEqualizerPreset(presetIndex: Int) {
427
+ val presets = listOf(
428
+ listOf(0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f), // Flat
429
+ listOf(5f, 4f, 3f, 1f, -1f, 0f, 2f, 3f, 4f, 4f), // Rock
430
+ listOf(-1f, 1f, 3f, 4f, 3f, 1f, 0f, 1f, 2f, 2f), // Pop
431
+ listOf(3f, 2f, 1f, 2f, -1f, -1f, 0f, 1f, 2f, 3f), // Jazz
432
+ listOf(4f, 3f, 2f, 1f, -1f, -1f, 0f, 2f, 3f, 4f), // Classical
433
+ listOf(5f, 5f, 3f, 1f, -1f, 0f, 1f, 0f, 2f, 3f), // Hip Hop
434
+ listOf(4f, 4f, 2f, 0f, -2f, -1f, 0f, 2f, 4f, 4f), // Electronic
435
+ listOf(3f, 2f, 1f, 1f, 0f, 0f, 1f, 2f, 2f, 2f), // Acoustic
436
+ listOf(6f, 5f, 4f, 2f, 0f, 0f, 0f, 0f, 0f, 0f), // Bass Boost
437
+ listOf(0f, 0f, 0f, 0f, 0f, 1f, 2f, 4f, 5f, 6f), // Treble Boost
438
+ listOf(-2f, -1f, 0f, 2f, 4f, 4f, 3f, 1f, 0f, -1f), // Vocal
439
+ listOf(5f, 4f, 2f, 0f, -2f, -2f, 0f, 2f, 4f, 5f) // Loudness
440
+ )
441
+ if (presetIndex >= 0 && presetIndex < presets.size) {
442
+ setEqualizerBands(presets[presetIndex])
443
+ }
444
+ }
445
+
298
446
  fun togglePlaying() {
299
447
  if (exoPlayer.isPlaying) {
300
448
  pause()
@@ -35,6 +35,7 @@ public class RNTrackPlayer: NSObject, AudioSessionControllerDelegate {
35
35
  private var hasInitialized = false
36
36
  private let player = QueuedAudioPlayer()
37
37
  private let audioSessionController = AudioSessionController.shared
38
+ private var equalizerTap: EqualizerAudioTap?
38
39
  private var shouldEmitProgressEvent: Bool = false
39
40
  private var shouldResumePlaybackAfterInterruptionEnds: Bool = false
40
41
  private var forwardJumpInterval: NSNumber? = nil;
@@ -895,6 +896,107 @@ public class RNTrackPlayer: NSObject, AudioSessionControllerDelegate {
895
896
  ]
896
897
  )
897
898
  }
899
+
900
+ // MARK: - iOS Equalizer Methods
901
+
902
+ @objc(setEqualizerEnabled:resolver:rejecter:)
903
+ public func setEqualizerEnabled(enabled: Bool, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
904
+ if (rejectWhenNotInitialized(reject: reject)) { return }
905
+
906
+ if enabled {
907
+ if equalizerTap == nil {
908
+ equalizerTap = EqualizerAudioTap()
909
+ }
910
+ equalizerTap?.isEnabled = true
911
+ player.audioTap = equalizerTap
912
+ } else {
913
+ equalizerTap?.isEnabled = false
914
+ player.audioTap = nil
915
+ }
916
+
917
+ resolve(NSNull())
918
+ }
919
+
920
+ @objc(getEqualizerEnabled:rejecter:)
921
+ public func getEqualizerEnabled(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
922
+ if (rejectWhenNotInitialized(reject: reject)) { return }
923
+
924
+ let enabled = equalizerTap?.isEnabled ?? false
925
+ resolve(enabled)
926
+ }
927
+
928
+ @objc(setEqualizerBand:gain:resolver:rejecter:)
929
+ public func setEqualizerBand(band: Int, gain: Float, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
930
+ if (rejectWhenNotInitialized(reject: reject)) { return }
931
+
932
+ if equalizerTap == nil {
933
+ equalizerTap = EqualizerAudioTap()
934
+ player.audioTap = equalizerTap
935
+ }
936
+
937
+ equalizerTap?.setGain(band: band, gainDB: gain)
938
+ resolve(NSNull())
939
+ }
940
+
941
+ @objc(setEqualizerBands:resolver:rejecter:)
942
+ public func setEqualizerBands(gains: [NSNumber], resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
943
+ if (rejectWhenNotInitialized(reject: reject)) { return }
944
+
945
+ if equalizerTap == nil {
946
+ equalizerTap = EqualizerAudioTap()
947
+ player.audioTap = equalizerTap
948
+ }
949
+
950
+ let floatGains = gains.map { $0.floatValue }
951
+ equalizerTap?.setAllGains(floatGains)
952
+ resolve(NSNull())
953
+ }
954
+
955
+ @objc(getEqualizerBands:rejecter:)
956
+ public func getEqualizerBands(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
957
+ if (rejectWhenNotInitialized(reject: reject)) { return }
958
+
959
+ let gains = equalizerTap?.getAllGains() ?? Array(repeating: Float(0), count: 10)
960
+ resolve(gains)
961
+ }
962
+
963
+ @objc(getEqualizerFrequencies:rejecter:)
964
+ public func getEqualizerFrequencies(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
965
+ resolve(EqualizerAudioTap.frequencies)
966
+ }
967
+
968
+ @objc(applyEqualizerPreset:resolver:rejecter:)
969
+ public func applyEqualizerPreset(presetIndex: Int, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
970
+ if (rejectWhenNotInitialized(reject: reject)) { return }
971
+
972
+ let presets = EqualizerAudioTap.Preset.allCases
973
+ guard presetIndex >= 0 && presetIndex < presets.count else {
974
+ reject("invalid_preset", "Preset index out of bounds", nil)
975
+ return
976
+ }
977
+
978
+ if equalizerTap == nil {
979
+ equalizerTap = EqualizerAudioTap()
980
+ player.audioTap = equalizerTap
981
+ }
982
+
983
+ let preset = presets[presetIndex]
984
+ equalizerTap?.applyPreset(preset)
985
+ resolve(NSNull())
986
+ }
987
+
988
+ @objc(getEqualizerPresetNames:rejecter:)
989
+ public func getEqualizerPresetNames(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
990
+ resolve(EqualizerAudioTap.presetNames)
991
+ }
992
+
993
+ @objc(resetEqualizer:rejecter:)
994
+ public func resetEqualizer(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
995
+ if (rejectWhenNotInitialized(reject: reject)) { return }
996
+
997
+ equalizerTap?.resetGains()
998
+ resolve(NSNull())
999
+ }
898
1000
  }
899
1001
 
900
1002
  extension RNTrackPlayer {
@@ -40,6 +40,7 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
40
40
  label: "AVPlayerWrapper.stateQueue",
41
41
  attributes: .concurrent
42
42
  )
43
+ private var cachedDuration: TimeInterval = 0.0
43
44
 
44
45
  public init() {
45
46
  playerTimeObserver = AVPlayerTimeObserver(periodicObserverTimeInterval: timeEventFrequency.getTime())
@@ -115,17 +116,8 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
115
116
  }
116
117
 
117
118
  var duration: TimeInterval {
118
- if let seconds = currentItem?.asset.duration.seconds, !seconds.isNaN {
119
- return seconds
120
- }
121
- else if let seconds = currentItem?.duration.seconds, !seconds.isNaN {
122
- return seconds
123
- }
124
- else if let seconds = currentItem?.seekableTimeRanges.last?.timeRangeValue.duration.seconds,
125
- !seconds.isNaN {
126
- return seconds
127
- }
128
- return 0.0
119
+ // Return cached duration to avoid synchronous access to AVAsset properties
120
+ return cachedDuration
129
121
  }
130
122
 
131
123
  var bufferedPosition: TimeInterval {
@@ -242,7 +234,7 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
242
234
  state = .loading
243
235
 
244
236
  // Load metadata keys asynchronously and separate from playable, to allow that to execute as quickly as it can
245
- let metdataKeys = ["commonMetadata", "availableChapterLocales", "availableMetadataFormats"]
237
+ let metdataKeys = ["commonMetadata", "availableChapterLocales", "availableMetadataFormats", "duration"]
246
238
  pendingAsset.loadValuesAsynchronously(forKeys: metdataKeys, completionHandler: { [weak self] in
247
239
  guard let self = self else { return }
248
240
  if (pendingAsset != self.asset) { return; }
@@ -381,6 +373,7 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
381
373
 
382
374
  asset.cancelLoading()
383
375
  self.asset = nil
376
+ cachedDuration = 0.0
384
377
 
385
378
  avPlayer.replaceCurrentItem(with: nil)
386
379
  }
@@ -518,6 +511,7 @@ extension AVPlayerWrapper: AVPlayerItemObserverDelegate {
518
511
  }
519
512
 
520
513
  func item(didUpdateDuration duration: Double) {
514
+ cachedDuration = duration
521
515
  delegate?.AVWrapper(didUpdateDuration: duration)
522
516
  }
523
517
 
@@ -0,0 +1,346 @@
1
+ //
2
+ // EqualizerAudioTap.swift
3
+ // SwiftAudioEx
4
+ //
5
+ // iOS Equalizer implementation using biquad peaking filters
6
+ //
7
+
8
+ import Foundation
9
+ import AVFoundation
10
+ import Accelerate
11
+
12
+ /// 10-band parametric equalizer using cascaded biquad filters
13
+ /// Processes audio in real-time via MTAudioProcessingTap
14
+ public class EqualizerAudioTap: AudioTap {
15
+
16
+ // MARK: - Constants
17
+
18
+ /// Standard 10-band equalizer frequencies (Hz)
19
+ public static let frequencies: [Float] = [
20
+ 32, // Sub-bass
21
+ 64, // Bass
22
+ 125, // Low-bass
23
+ 250, // Low-mid
24
+ 500, // Mid
25
+ 1000, // Mid
26
+ 2000, // Upper-mid
27
+ 4000, // Presence
28
+ 8000, // Brilliance
29
+ 16000 // Air
30
+ ]
31
+
32
+ /// Number of equalizer bands
33
+ public static let bandCount = 10
34
+
35
+ /// Default Q factor for peaking filters (standard for graphic EQ)
36
+ private static let defaultQ: Float = 1.41
37
+
38
+ // MARK: - Properties
39
+
40
+ /// Whether the equalizer is enabled
41
+ public var isEnabled: Bool = true
42
+
43
+ /// Gain values for each band in dB (-12 to +12)
44
+ private var _gains: [Float] = Array(repeating: 0, count: bandCount)
45
+
46
+ /// Thread-safe access to gains
47
+ private let gainsLock = NSLock()
48
+
49
+ /// Current sample rate (set during prepare)
50
+ private var sampleRate: Float = 44100
51
+
52
+ /// Biquad filter coefficients for each band [b0, b1, b2, a1, a2]
53
+ private var coefficients: [[Float]] = []
54
+
55
+ /// Filter state (delay elements) for each band, per channel
56
+ /// Structure: [band][channel][z1, z2]
57
+ private var filterStates: [[[Float]]] = []
58
+
59
+ /// Number of audio channels
60
+ private var channelCount: Int = 2
61
+
62
+ /// Flag to indicate coefficients need recalculation
63
+ private var needsUpdate: Bool = true
64
+
65
+ // MARK: - Public API
66
+
67
+ public override init() {
68
+ super.init()
69
+ // Initialize with flat response (all gains at 0)
70
+ resetGains()
71
+ }
72
+
73
+ /// Set gain for a specific band
74
+ /// - Parameters:
75
+ /// - band: Band index (0-9)
76
+ /// - gainDB: Gain in decibels (-12 to +12)
77
+ public func setGain(band: Int, gainDB: Float) {
78
+ guard band >= 0 && band < Self.bandCount else { return }
79
+
80
+ let clampedGain = max(-12, min(12, gainDB))
81
+
82
+ gainsLock.lock()
83
+ _gains[band] = clampedGain
84
+ needsUpdate = true
85
+ gainsLock.unlock()
86
+ }
87
+
88
+ /// Set gains for all bands at once
89
+ /// - Parameter gains: Array of 10 gain values in dB
90
+ public func setAllGains(_ gains: [Float]) {
91
+ guard gains.count == Self.bandCount else { return }
92
+
93
+ gainsLock.lock()
94
+ for i in 0..<Self.bandCount {
95
+ _gains[i] = max(-12, min(12, gains[i]))
96
+ }
97
+ needsUpdate = true
98
+ gainsLock.unlock()
99
+ }
100
+
101
+ /// Get current gain for a specific band
102
+ public func getGain(band: Int) -> Float {
103
+ guard band >= 0 && band < Self.bandCount else { return 0 }
104
+
105
+ gainsLock.lock()
106
+ let gain = _gains[band]
107
+ gainsLock.unlock()
108
+ return gain
109
+ }
110
+
111
+ /// Get all current gains
112
+ public func getAllGains() -> [Float] {
113
+ gainsLock.lock()
114
+ let gains = _gains
115
+ gainsLock.unlock()
116
+ return gains
117
+ }
118
+
119
+ /// Reset all gains to 0 (flat response)
120
+ public func resetGains() {
121
+ gainsLock.lock()
122
+ _gains = Array(repeating: 0, count: Self.bandCount)
123
+ needsUpdate = true
124
+ gainsLock.unlock()
125
+ }
126
+
127
+ // MARK: - AudioTap Overrides
128
+
129
+ public override func initialize() {
130
+ // Called when tap is attached
131
+ }
132
+
133
+ public override func finalize() {
134
+ // Called when tap is detached
135
+ filterStates = []
136
+ coefficients = []
137
+ }
138
+
139
+ public override func prepare(description: AudioStreamBasicDescription) {
140
+ sampleRate = Float(description.mSampleRate)
141
+ channelCount = Int(description.mChannelsPerFrame)
142
+
143
+ // Initialize filter states for each band and channel
144
+ filterStates = Array(
145
+ repeating: Array(
146
+ repeating: [0, 0], // z1, z2 delay elements
147
+ count: channelCount
148
+ ),
149
+ count: Self.bandCount
150
+ )
151
+
152
+ // Calculate initial coefficients
153
+ updateCoefficients()
154
+ }
155
+
156
+ public override func unprepare() {
157
+ // Reset filter states
158
+ for band in 0..<filterStates.count {
159
+ for channel in 0..<filterStates[band].count {
160
+ filterStates[band][channel] = [0, 0]
161
+ }
162
+ }
163
+ }
164
+
165
+ public override func process(numberOfFrames: Int, buffer: UnsafeMutableAudioBufferListPointer) {
166
+ // Skip processing if disabled or no frames
167
+ guard isEnabled && numberOfFrames > 0 else { return }
168
+
169
+ // Check if coefficients need update
170
+ gainsLock.lock()
171
+ if needsUpdate {
172
+ updateCoefficientsLocked()
173
+ needsUpdate = false
174
+ }
175
+ let currentCoefficients = coefficients
176
+ gainsLock.unlock()
177
+
178
+ // Process each channel
179
+ for channelIndex in 0..<min(buffer.count, channelCount) {
180
+ guard let channelData = buffer[channelIndex].mData else { continue }
181
+
182
+ let samples = channelData.assumingMemoryBound(to: Float.self)
183
+
184
+ // Apply each band's filter in cascade
185
+ for bandIndex in 0..<Self.bandCount {
186
+ guard bandIndex < currentCoefficients.count else { continue }
187
+
188
+ let coeff = currentCoefficients[bandIndex]
189
+ guard coeff.count >= 5 else { continue }
190
+
191
+ let b0 = coeff[0]
192
+ let b1 = coeff[1]
193
+ let b2 = coeff[2]
194
+ let a1 = coeff[3]
195
+ let a2 = coeff[4]
196
+
197
+ // Get filter state for this band and channel
198
+ var z1 = filterStates[bandIndex][channelIndex][0]
199
+ var z2 = filterStates[bandIndex][channelIndex][1]
200
+
201
+ // Process samples using Direct Form II Transposed
202
+ for i in 0..<numberOfFrames {
203
+ let input = samples[i]
204
+ let output = b0 * input + z1
205
+ z1 = b1 * input - a1 * output + z2
206
+ z2 = b2 * input - a2 * output
207
+ samples[i] = output
208
+ }
209
+
210
+ // Save filter state
211
+ filterStates[bandIndex][channelIndex][0] = z1
212
+ filterStates[bandIndex][channelIndex][1] = z2
213
+ }
214
+ }
215
+ }
216
+
217
+ // MARK: - Private Methods
218
+
219
+ private func updateCoefficients() {
220
+ gainsLock.lock()
221
+ updateCoefficientsLocked()
222
+ gainsLock.unlock()
223
+ }
224
+
225
+ /// Calculate biquad coefficients for all bands (must be called with lock held)
226
+ private func updateCoefficientsLocked() {
227
+ coefficients = []
228
+
229
+ for i in 0..<Self.bandCount {
230
+ let freq = Self.frequencies[i]
231
+ let gainDB = _gains[i]
232
+
233
+ let coeff = calculatePeakingEQCoefficients(
234
+ frequency: freq,
235
+ gainDB: gainDB,
236
+ q: Self.defaultQ,
237
+ sampleRate: sampleRate
238
+ )
239
+ coefficients.append(coeff)
240
+ }
241
+ }
242
+
243
+ /// Calculate biquad coefficients for a peaking EQ filter
244
+ /// Based on Audio EQ Cookbook by Robert Bristow-Johnson
245
+ /// - Parameters:
246
+ /// - frequency: Center frequency in Hz
247
+ /// - gainDB: Gain in decibels
248
+ /// - q: Q factor (bandwidth)
249
+ /// - sampleRate: Sample rate in Hz
250
+ /// - Returns: Coefficients [b0, b1, b2, a1, a2] (normalized by a0)
251
+ private func calculatePeakingEQCoefficients(
252
+ frequency: Float,
253
+ gainDB: Float,
254
+ q: Float,
255
+ sampleRate: Float
256
+ ) -> [Float] {
257
+ // If gain is essentially 0, return unity (pass-through)
258
+ if abs(gainDB) < 0.01 {
259
+ return [1, 0, 0, 0, 0] // b0=1, rest=0 means y[n] = x[n]
260
+ }
261
+
262
+ let A = pow(10, gainDB / 40) // sqrt(10^(dB/20))
263
+ let w0 = 2 * Float.pi * frequency / sampleRate
264
+ let cosW0 = cos(w0)
265
+ let sinW0 = sin(w0)
266
+ let alpha = sinW0 / (2 * q)
267
+
268
+ // Peaking EQ coefficients
269
+ let b0 = 1 + alpha * A
270
+ let b1 = -2 * cosW0
271
+ let b2 = 1 - alpha * A
272
+ let a0 = 1 + alpha / A
273
+ let a1 = -2 * cosW0
274
+ let a2 = 1 - alpha / A
275
+
276
+ // Normalize by a0
277
+ return [
278
+ b0 / a0,
279
+ b1 / a0,
280
+ b2 / a0,
281
+ a1 / a0,
282
+ a2 / a0
283
+ ]
284
+ }
285
+ }
286
+
287
+ // MARK: - Preset Support
288
+
289
+ extension EqualizerAudioTap {
290
+
291
+ /// Predefined equalizer presets
292
+ public enum Preset: String, CaseIterable {
293
+ case flat = "Flat"
294
+ case rock = "Rock"
295
+ case pop = "Pop"
296
+ case jazz = "Jazz"
297
+ case classical = "Classical"
298
+ case hiphop = "Hip Hop"
299
+ case electronic = "Electronic"
300
+ case acoustic = "Acoustic"
301
+ case bass = "Bass Boost"
302
+ case treble = "Treble Boost"
303
+ case vocal = "Vocal"
304
+ case loudness = "Loudness"
305
+
306
+ /// Gain values for each preset (10 bands)
307
+ public var gains: [Float] {
308
+ switch self {
309
+ case .flat:
310
+ return [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
311
+ case .rock:
312
+ return [5, 4, 3, 1, -1, 0, 2, 3, 4, 4]
313
+ case .pop:
314
+ return [-1, 1, 3, 4, 3, 1, 0, 1, 2, 2]
315
+ case .jazz:
316
+ return [3, 2, 1, 2, -1, -1, 0, 1, 2, 3]
317
+ case .classical:
318
+ return [4, 3, 2, 1, -1, -1, 0, 2, 3, 4]
319
+ case .hiphop:
320
+ return [5, 5, 3, 1, -1, 0, 1, 0, 2, 3]
321
+ case .electronic:
322
+ return [4, 4, 2, 0, -2, -1, 0, 2, 4, 4]
323
+ case .acoustic:
324
+ return [3, 2, 1, 1, 0, 0, 1, 2, 2, 2]
325
+ case .bass:
326
+ return [6, 5, 4, 2, 0, 0, 0, 0, 0, 0]
327
+ case .treble:
328
+ return [0, 0, 0, 0, 0, 1, 2, 4, 5, 6]
329
+ case .vocal:
330
+ return [-2, -1, 0, 2, 4, 4, 3, 1, 0, -1]
331
+ case .loudness:
332
+ return [5, 4, 2, 0, -2, -2, 0, 2, 4, 5]
333
+ }
334
+ }
335
+ }
336
+
337
+ /// Apply a preset
338
+ public func applyPreset(_ preset: Preset) {
339
+ setAllGains(preset.gains)
340
+ }
341
+
342
+ /// Get all available preset names
343
+ public static var presetNames: [String] {
344
+ return Preset.allCases.map { $0.rawValue }
345
+ }
346
+ }
@@ -46,6 +46,15 @@ export interface Spec extends TurboModule {
46
46
  setEqualizerPreset(preset: number): Promise<void>;
47
47
  getCurrentEqualizerPreset(): Promise<number>;
48
48
  getEqualizerPresets(): Promise<string[]>;
49
+ setEqualizerEnabled(enabled: boolean): Promise<void>;
50
+ getEqualizerEnabled(): Promise<boolean>;
51
+ setEqualizerBand(band: number, gain: number): Promise<void>;
52
+ setEqualizerBands(gains: number[]): Promise<void>;
53
+ getEqualizerBands(): Promise<number[]>;
54
+ getEqualizerFrequencies(): Promise<number[]>;
55
+ applyEqualizerPreset(presetIndex: number): Promise<void>;
56
+ getEqualizerPresetNames(): Promise<string[]>;
57
+ resetEqualizer(): Promise<void>;
49
58
  addListener(eventName: string): void;
50
59
  removeListeners(count: number): void;
51
60
  getConstants: () => {
@@ -369,3 +369,51 @@ export declare function getCurrentEqualizerPreset(): Promise<number>;
369
369
  * android only. get the current eq preset names.
370
370
  */
371
371
  export declare function getEqualizerPresets(): Promise<string[]>;
372
+ /**
373
+ * Enable or disable the parametric equalizer.
374
+ * - iOS: 10-band biquad peaking filters
375
+ * - Android: System equalizer (band count varies by device, typically 5)
376
+ */
377
+ export declare function setEqualizerEnabled(enabled: boolean): Promise<void>;
378
+ /**
379
+ * Check if the equalizer is enabled.
380
+ */
381
+ export declare function getEqualizerEnabled(): Promise<boolean>;
382
+ /**
383
+ * Set the gain for a specific equalizer band.
384
+ * @param band Band index (0 to bandCount-1)
385
+ * @param gain Gain in dB (range varies by platform, typically -12 to +12)
386
+ */
387
+ export declare function setEqualizerBand(band: number, gain: number): Promise<void>;
388
+ /**
389
+ * Set all equalizer band gains at once.
390
+ * @param gains Array of gain values in dB
391
+ * Note: iOS has 10 bands, Android typically has 5 (device dependent)
392
+ */
393
+ export declare function setEqualizerBands(gains: number[]): Promise<void>;
394
+ /**
395
+ * Get all current equalizer band gains.
396
+ * @returns Array of gain values in dB
397
+ */
398
+ export declare function getEqualizerBands(): Promise<number[]>;
399
+ /**
400
+ * Get the center frequencies for each equalizer band.
401
+ * @returns Array of frequency values in Hz
402
+ * - iOS: [32, 64, 125, 250, 500, 1000, 2000, 4000, 8000, 16000]
403
+ * - Android: varies by device (typically 5 bands)
404
+ */
405
+ export declare function getEqualizerFrequencies(): Promise<number[]>;
406
+ /**
407
+ * Apply a preset to the equalizer.
408
+ * @param presetIndex Index of the preset (use getEqualizerPresetNames to get available presets)
409
+ */
410
+ export declare function applyEqualizerPreset(presetIndex: number): Promise<void>;
411
+ /**
412
+ * Get available equalizer preset names.
413
+ * @returns Array of preset names ["Flat", "Rock", "Pop", "Jazz", ...]
414
+ */
415
+ export declare function getEqualizerPresetNames(): Promise<string[]>;
416
+ /**
417
+ * Reset all equalizer bands to 0 (flat response).
418
+ */
419
+ export declare function resetEqualizer(): Promise<void>;
@@ -625,3 +625,70 @@ export async function getEqualizerPresets() {
625
625
  return [];
626
626
  return TrackPlayer.getEqualizerPresets();
627
627
  }
628
+ // MARK: - Cross-Platform Equalizer API (Band Control)
629
+ /**
630
+ * Enable or disable the parametric equalizer.
631
+ * - iOS: 10-band biquad peaking filters
632
+ * - Android: System equalizer (band count varies by device, typically 5)
633
+ */
634
+ export async function setEqualizerEnabled(enabled) {
635
+ return TrackPlayer.setEqualizerEnabled(enabled);
636
+ }
637
+ /**
638
+ * Check if the equalizer is enabled.
639
+ */
640
+ export async function getEqualizerEnabled() {
641
+ return TrackPlayer.getEqualizerEnabled();
642
+ }
643
+ /**
644
+ * Set the gain for a specific equalizer band.
645
+ * @param band Band index (0 to bandCount-1)
646
+ * @param gain Gain in dB (range varies by platform, typically -12 to +12)
647
+ */
648
+ export async function setEqualizerBand(band, gain) {
649
+ return TrackPlayer.setEqualizerBand(band, gain);
650
+ }
651
+ /**
652
+ * Set all equalizer band gains at once.
653
+ * @param gains Array of gain values in dB
654
+ * Note: iOS has 10 bands, Android typically has 5 (device dependent)
655
+ */
656
+ export async function setEqualizerBands(gains) {
657
+ return TrackPlayer.setEqualizerBands(gains);
658
+ }
659
+ /**
660
+ * Get all current equalizer band gains.
661
+ * @returns Array of gain values in dB
662
+ */
663
+ export async function getEqualizerBands() {
664
+ return TrackPlayer.getEqualizerBands();
665
+ }
666
+ /**
667
+ * Get the center frequencies for each equalizer band.
668
+ * @returns Array of frequency values in Hz
669
+ * - iOS: [32, 64, 125, 250, 500, 1000, 2000, 4000, 8000, 16000]
670
+ * - Android: varies by device (typically 5 bands)
671
+ */
672
+ export async function getEqualizerFrequencies() {
673
+ return TrackPlayer.getEqualizerFrequencies();
674
+ }
675
+ /**
676
+ * Apply a preset to the equalizer.
677
+ * @param presetIndex Index of the preset (use getEqualizerPresetNames to get available presets)
678
+ */
679
+ export async function applyEqualizerPreset(presetIndex) {
680
+ return TrackPlayer.applyEqualizerPreset(presetIndex);
681
+ }
682
+ /**
683
+ * Get available equalizer preset names.
684
+ * @returns Array of preset names ["Flat", "Rock", "Pop", "Jazz", ...]
685
+ */
686
+ export async function getEqualizerPresetNames() {
687
+ return TrackPlayer.getEqualizerPresetNames();
688
+ }
689
+ /**
690
+ * Reset all equalizer bands to 0 (flat response).
691
+ */
692
+ export async function resetEqualizer() {
693
+ return TrackPlayer.resetEqualizer();
694
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@javascriptcommon/react-native-track-player",
3
- "version": "4.1.9",
3
+ "version": "4.1.12",
4
4
  "description": "A fully fledged audio module created for music apps",
5
5
  "main": "lib/src/index.js",
6
6
  "types": "lib/src/index.d.ts",
@@ -68,6 +68,17 @@ export interface Spec extends TurboModule {
68
68
  getCurrentEqualizerPreset(): Promise<number>;
69
69
  getEqualizerPresets(): Promise<string[]>;
70
70
 
71
+ // iOS Equalizer methods (10-band parametric EQ)
72
+ setEqualizerEnabled(enabled: boolean): Promise<void>;
73
+ getEqualizerEnabled(): Promise<boolean>;
74
+ setEqualizerBand(band: number, gain: number): Promise<void>;
75
+ setEqualizerBands(gains: number[]): Promise<void>;
76
+ getEqualizerBands(): Promise<number[]>;
77
+ getEqualizerFrequencies(): Promise<number[]>;
78
+ applyEqualizerPreset(presetIndex: number): Promise<void>;
79
+ getEqualizerPresetNames(): Promise<string[]>;
80
+ resetEqualizer(): Promise<void>;
81
+
71
82
  // event listeners
72
83
  addListener(eventName: string): void;
73
84
  removeListeners(count: number): void;
@@ -804,3 +804,83 @@ export async function getEqualizerPresets(): Promise<string[]> {
804
804
  if (!isAndroid) return [];
805
805
  return TrackPlayer.getEqualizerPresets();
806
806
  }
807
+
808
+ // MARK: - Cross-Platform Equalizer API (Band Control)
809
+
810
+ /**
811
+ * Enable or disable the parametric equalizer.
812
+ * - iOS: 10-band biquad peaking filters
813
+ * - Android: System equalizer (band count varies by device, typically 5)
814
+ */
815
+ export async function setEqualizerEnabled(enabled: boolean): Promise<void> {
816
+ return TrackPlayer.setEqualizerEnabled(enabled);
817
+ }
818
+
819
+ /**
820
+ * Check if the equalizer is enabled.
821
+ */
822
+ export async function getEqualizerEnabled(): Promise<boolean> {
823
+ return TrackPlayer.getEqualizerEnabled();
824
+ }
825
+
826
+ /**
827
+ * Set the gain for a specific equalizer band.
828
+ * @param band Band index (0 to bandCount-1)
829
+ * @param gain Gain in dB (range varies by platform, typically -12 to +12)
830
+ */
831
+ export async function setEqualizerBand(
832
+ band: number,
833
+ gain: number,
834
+ ): Promise<void> {
835
+ return TrackPlayer.setEqualizerBand(band, gain);
836
+ }
837
+
838
+ /**
839
+ * Set all equalizer band gains at once.
840
+ * @param gains Array of gain values in dB
841
+ * Note: iOS has 10 bands, Android typically has 5 (device dependent)
842
+ */
843
+ export async function setEqualizerBands(gains: number[]): Promise<void> {
844
+ return TrackPlayer.setEqualizerBands(gains);
845
+ }
846
+
847
+ /**
848
+ * Get all current equalizer band gains.
849
+ * @returns Array of gain values in dB
850
+ */
851
+ export async function getEqualizerBands(): Promise<number[]> {
852
+ return TrackPlayer.getEqualizerBands();
853
+ }
854
+
855
+ /**
856
+ * Get the center frequencies for each equalizer band.
857
+ * @returns Array of frequency values in Hz
858
+ * - iOS: [32, 64, 125, 250, 500, 1000, 2000, 4000, 8000, 16000]
859
+ * - Android: varies by device (typically 5 bands)
860
+ */
861
+ export async function getEqualizerFrequencies(): Promise<number[]> {
862
+ return TrackPlayer.getEqualizerFrequencies();
863
+ }
864
+
865
+ /**
866
+ * Apply a preset to the equalizer.
867
+ * @param presetIndex Index of the preset (use getEqualizerPresetNames to get available presets)
868
+ */
869
+ export async function applyEqualizerPreset(presetIndex: number): Promise<void> {
870
+ return TrackPlayer.applyEqualizerPreset(presetIndex);
871
+ }
872
+
873
+ /**
874
+ * Get available equalizer preset names.
875
+ * @returns Array of preset names ["Flat", "Rock", "Pop", "Jazz", ...]
876
+ */
877
+ export async function getEqualizerPresetNames(): Promise<string[]> {
878
+ return TrackPlayer.getEqualizerPresetNames();
879
+ }
880
+
881
+ /**
882
+ * Reset all equalizer bands to 0 (flat response).
883
+ */
884
+ export async function resetEqualizer(): Promise<void> {
885
+ return TrackPlayer.resetEqualizer();
886
+ }