react-native-nitro-player 0.7.0 → 0.7.1-alpha.1

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.
Files changed (110) hide show
  1. package/README.md +47 -46
  2. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAndroidAutoMediaLibrary.kt +9 -13
  3. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAudioDevices.kt +45 -90
  4. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridDownloadManager.kt +48 -182
  5. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridEqualizer.kt +21 -77
  6. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridPlayerQueue.kt +55 -104
  7. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridTrackPlayer.kt +113 -123
  8. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/ExoPlayerCore.kt +82 -0
  9. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/ListenerRegistry.kt +48 -0
  10. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerAndroidAuto.kt +62 -0
  11. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCore.kt +153 -1887
  12. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerListener.kt +122 -0
  13. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerNotify.kt +44 -0
  14. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerPlayback.kt +162 -0
  15. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerQueue.kt +179 -0
  16. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerQueueBuild.kt +170 -0
  17. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerSetup.kt +28 -0
  18. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerTempQueue.kt +121 -0
  19. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerUrlLoader.kt +98 -0
  20. package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadDatabase.kt +27 -18
  21. package/android/src/main/java/com/margelo/nitro/nitroplayer/equalizer/EqualizerCore.kt +150 -135
  22. package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaSessionManager.kt +13 -30
  23. package/android/src/main/java/com/margelo/nitro/nitroplayer/playlist/PlaylistManager.kt +102 -162
  24. package/ios/HybridDownloadManager.swift +32 -26
  25. package/ios/HybridEqualizer.swift +48 -35
  26. package/ios/HybridTrackPlayer.swift +127 -102
  27. package/ios/core/ListenerRegistry.swift +60 -0
  28. package/ios/core/TrackPlayerCore.swift +130 -2356
  29. package/ios/core/TrackPlayerListener.swift +395 -0
  30. package/ios/core/TrackPlayerNotify.swift +52 -0
  31. package/ios/core/TrackPlayerPlayback.swift +274 -0
  32. package/ios/core/TrackPlayerQueue.swift +221 -0
  33. package/ios/core/TrackPlayerQueueBuild.swift +493 -0
  34. package/ios/core/TrackPlayerTempQueue.swift +167 -0
  35. package/ios/core/TrackPlayerUrlLoader.swift +169 -0
  36. package/ios/equalizer/EqualizerCore.swift +63 -123
  37. package/ios/media/MediaSessionManager.swift +32 -49
  38. package/ios/playlist/PlaylistManager.swift +2 -9
  39. package/ios/queue/HybridPlayerQueue.swift +69 -66
  40. package/lib/hooks/useDownloadedTracks.js +16 -13
  41. package/lib/hooks/useEqualizer.d.ts +4 -4
  42. package/lib/hooks/useEqualizer.js +22 -17
  43. package/lib/hooks/useEqualizerPresets.d.ts +3 -3
  44. package/lib/hooks/useEqualizerPresets.js +12 -18
  45. package/lib/specs/AndroidAutoMediaLibrary.nitro.d.ts +2 -2
  46. package/lib/specs/AudioDevices.nitro.d.ts +2 -2
  47. package/lib/specs/DownloadManager.nitro.d.ts +10 -10
  48. package/lib/specs/Equalizer.nitro.d.ts +10 -10
  49. package/lib/specs/TrackPlayer.nitro.d.ts +38 -16
  50. package/lib/types/EqualizerTypes.d.ts +3 -3
  51. package/nitrogen/generated/android/NitroPlayerOnLoad.cpp +2 -0
  52. package/nitrogen/generated/android/c++/JFunc_void_std__vector_TrackItem__std__vector_TrackItem_.hpp +122 -0
  53. package/nitrogen/generated/android/c++/JHybridAndroidAutoMediaLibrarySpec.cpp +31 -6
  54. package/nitrogen/generated/android/c++/JHybridAndroidAutoMediaLibrarySpec.hpp +2 -2
  55. package/nitrogen/generated/android/c++/JHybridAudioDevicesSpec.cpp +16 -3
  56. package/nitrogen/generated/android/c++/JHybridAudioDevicesSpec.hpp +1 -1
  57. package/nitrogen/generated/android/c++/JHybridDownloadManagerSpec.cpp +154 -44
  58. package/nitrogen/generated/android/c++/JHybridDownloadManagerSpec.hpp +10 -10
  59. package/nitrogen/generated/android/c++/JHybridEqualizerSpec.cpp +130 -34
  60. package/nitrogen/generated/android/c++/JHybridEqualizerSpec.hpp +9 -9
  61. package/nitrogen/generated/android/c++/JHybridPlayerQueueSpec.cpp +115 -24
  62. package/nitrogen/generated/android/c++/JHybridPlayerQueueSpec.hpp +8 -8
  63. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +243 -24
  64. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +16 -8
  65. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_std__vector_TrackItem__std__vector_TrackItem_.kt +80 -0
  66. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridAndroidAutoMediaLibrarySpec.kt +3 -2
  67. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridAudioDevicesSpec.kt +2 -1
  68. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridDownloadManagerSpec.kt +10 -10
  69. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridEqualizerSpec.kt +10 -9
  70. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridPlayerQueueSpec.kt +9 -8
  71. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridTrackPlayerSpec.kt +45 -8
  72. package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.cpp +74 -18
  73. package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.hpp +380 -151
  74. package/nitrogen/generated/ios/c++/HybridDownloadManagerSpecSwift.hpp +10 -10
  75. package/nitrogen/generated/ios/c++/HybridEqualizerSpecSwift.hpp +12 -9
  76. package/nitrogen/generated/ios/c++/HybridPlayerQueueSpecSwift.hpp +23 -8
  77. package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +82 -8
  78. package/nitrogen/generated/ios/swift/Func_void_EqualizerState.swift +46 -0
  79. package/nitrogen/generated/ios/swift/Func_void_std__variant_nitro__NullType__DownloadedPlaylist_.swift +58 -0
  80. package/nitrogen/generated/ios/swift/Func_void_std__variant_nitro__NullType__DownloadedTrack_.swift +58 -0
  81. package/nitrogen/generated/ios/swift/Func_void_std__variant_nitro__NullType__std__string_.swift +58 -0
  82. package/nitrogen/generated/ios/swift/Func_void_std__vector_DownloadedPlaylist_.swift +46 -0
  83. package/nitrogen/generated/ios/swift/Func_void_std__vector_DownloadedTrack_.swift +46 -0
  84. package/nitrogen/generated/ios/swift/Func_void_std__vector_EqualizerBand_.swift +5 -5
  85. package/nitrogen/generated/ios/swift/Func_void_std__vector_TrackItem__std__vector_TrackItem_.swift +46 -0
  86. package/nitrogen/generated/ios/swift/HybridDownloadManagerSpec.swift +10 -10
  87. package/nitrogen/generated/ios/swift/HybridDownloadManagerSpec_cxx.swift +141 -71
  88. package/nitrogen/generated/ios/swift/HybridEqualizerSpec.swift +9 -9
  89. package/nitrogen/generated/ios/swift/HybridEqualizerSpec_cxx.swift +105 -41
  90. package/nitrogen/generated/ios/swift/HybridPlayerQueueSpec.swift +8 -8
  91. package/nitrogen/generated/ios/swift/HybridPlayerQueueSpec_cxx.swift +95 -32
  92. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +16 -8
  93. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +267 -32
  94. package/nitrogen/generated/shared/c++/HybridAndroidAutoMediaLibrarySpec.hpp +3 -2
  95. package/nitrogen/generated/shared/c++/HybridAudioDevicesSpec.hpp +2 -1
  96. package/nitrogen/generated/shared/c++/HybridDownloadManagerSpec.hpp +10 -10
  97. package/nitrogen/generated/shared/c++/HybridEqualizerSpec.hpp +10 -9
  98. package/nitrogen/generated/shared/c++/HybridPlayerQueueSpec.hpp +9 -8
  99. package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.cpp +8 -0
  100. package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +16 -8
  101. package/package.json +5 -5
  102. package/src/hooks/useDownloadedTracks.ts +17 -13
  103. package/src/hooks/useEqualizer.ts +26 -21
  104. package/src/hooks/useEqualizerPresets.ts +15 -21
  105. package/src/specs/AndroidAutoMediaLibrary.nitro.ts +2 -2
  106. package/src/specs/AudioDevices.nitro.ts +2 -2
  107. package/src/specs/DownloadManager.nitro.ts +10 -10
  108. package/src/specs/Equalizer.nitro.ts +10 -10
  109. package/src/specs/TrackPlayer.nitro.ts +52 -16
  110. package/src/types/EqualizerTypes.ts +17 -13
@@ -0,0 +1,169 @@
1
+ //
2
+ // TrackPlayerUrlLoader.swift
3
+ // NitroPlayer
4
+ //
5
+ // Created by Ritesh Shukla on 25/03/26.
6
+ //
7
+
8
+ import AVFoundation
9
+ import Foundation
10
+
11
+ extension TrackPlayerCore {
12
+
13
+ func updateTracks(tracks: [TrackItem]) async {
14
+ await withPlayerQueueNoThrow { self.updateTracksInternal(tracks: tracks) }
15
+ }
16
+
17
+ func getTracksById(trackIds: [String]) async -> [TrackItem] {
18
+ await withPlayerQueueNoThrow { self.playlistManager.getTracksById(trackIds: trackIds) }
19
+ }
20
+
21
+ func getTracksNeedingUrls() async -> [TrackItem] {
22
+ await withPlayerQueueNoThrow { self.getTracksNeedingUrlsInternal() }
23
+ }
24
+
25
+ func getNextTracks(count: Int) async -> [TrackItem] {
26
+ await withPlayerQueueNoThrow { self.getNextTracksInternal(count: count) }
27
+ }
28
+
29
+ // MARK: - Internal
30
+
31
+ func updateTracksInternal(tracks: [TrackItem]) {
32
+ NitroPlayerLogger.log("TrackPlayerCore", "🔄 updateTracks: \(tracks.count) updates")
33
+
34
+ let currentTrack = self.getCurrentTrack()
35
+ let currentTrackId = currentTrack?.id
36
+ // A track is only "empty" if it has no remote URL AND is not downloaded.
37
+ let currentTrackIsEmpty = currentTrack.map {
38
+ $0.url.isEmpty && !DownloadManagerCore.shared.isTrackDownloaded(trackId: $0.id)
39
+ } ?? false
40
+
41
+ let safeTracks = tracks.filter { track in
42
+ switch true {
43
+ case track.id == currentTrackId && !currentTrackIsEmpty:
44
+ NitroPlayerLogger.log("TrackPlayerCore",
45
+ "⚠️ Skipping update for currently playing track: \(track.id) (preserves gapless)")
46
+ return false
47
+ case track.id == currentTrackId && currentTrackIsEmpty:
48
+ NitroPlayerLogger.log("TrackPlayerCore",
49
+ "🔄 Updating current track with no URL: \(track.id)")
50
+ return !track.url.isEmpty
51
+ case track.url.isEmpty:
52
+ NitroPlayerLogger.log("TrackPlayerCore", "⚠️ Skipping track with empty URL: \(track.id)")
53
+ return false
54
+ default:
55
+ return true
56
+ }
57
+ }
58
+
59
+ guard !safeTracks.isEmpty else {
60
+ NitroPlayerLogger.log("TrackPlayerCore", "✅ No valid updates to apply")
61
+ return
62
+ }
63
+
64
+ // Invalidate preloaded assets for tracks with updated data
65
+ let updatedTrackIds = Set(safeTracks.map { $0.id })
66
+ for trackId in updatedTrackIds {
67
+ if self.preloadedAssets[trackId] != nil {
68
+ NitroPlayerLogger.log("TrackPlayerCore", "🗑️ Invalidating preloaded asset for track: \(trackId)")
69
+ self.preloadedAssets.removeValue(forKey: trackId)
70
+ }
71
+ }
72
+
73
+ // Update in PlaylistManager
74
+ let affectedPlaylists = self.playlistManager.updateTracks(tracks: safeTracks)
75
+
76
+ // If the current track had no URL and now has one, replace the current AVPlayerItem
77
+ if let update = currentTrack, currentTrackIsEmpty, !update.url.isEmpty {
78
+ NitroPlayerLogger.log("TrackPlayerCore",
79
+ "🔄 Replacing current AVPlayerItem for track with resolved URL: \(update.id)")
80
+ if let newItem = self.createGaplessPlayerItem(for: update, isPreload: false) {
81
+ self.player?.replaceCurrentItem(with: newItem)
82
+ }
83
+ }
84
+
85
+ // Rebuild queue if current playlist was affected
86
+ if let currentId = self.currentPlaylistId,
87
+ let updateCount = affectedPlaylists[currentId]
88
+ {
89
+ NitroPlayerLogger.log("TrackPlayerCore",
90
+ "🔄 Rebuilding queue - \(updateCount) tracks updated in current playlist")
91
+
92
+ // Sync currentTracks from the freshly-updated PlaylistManager
93
+ if let updatedPlaylist = self.playlistManager.getPlaylist(playlistId: currentId) {
94
+ self.currentTracks = updatedPlaylist.tracks
95
+ NitroPlayerLogger.log("TrackPlayerCore",
96
+ "📥 Synced currentTracks from PlaylistManager (\(self.currentTracks.count) tracks)")
97
+ }
98
+
99
+ if self.player?.currentItem == nil, let player = self.player {
100
+ // No AVPlayerItem exists yet — lazy-load mode: URLs were empty when the queue first loaded.
101
+ NitroPlayerLogger.log("TrackPlayerCore",
102
+ "🔄 No current item — full queue rebuild from currentTrackIndex \(self.currentTrackIndex)")
103
+ player.removeAllItems()
104
+ var lastItem: AVPlayerItem? = nil
105
+ for (offset, track) in self.currentTracks[max(0, self.currentTrackIndex)...].enumerated() {
106
+ let isPreload = offset < Constants.gaplessPreloadCount
107
+ if let newItem = self.createGaplessPlayerItem(for: track, isPreload: isPreload) {
108
+ player.insert(newItem, after: lastItem)
109
+ lastItem = newItem
110
+ }
111
+ }
112
+ player.play()
113
+ self.preloadUpcomingTracks(from: self.currentTrackIndex + 1)
114
+ } else {
115
+ // A current AVPlayerItem already exists — preserve it and only rebuild upcoming items.
116
+ self.rebuildAVQueueFromCurrentPosition(changedTrackIds: updatedTrackIds)
117
+ self.preloadUpcomingTracks(from: self.currentTrackIndex + 1)
118
+ }
119
+
120
+ NitroPlayerLogger.log("TrackPlayerCore", "✅ Queue rebuilt, gapless playback preserved")
121
+ }
122
+
123
+ NitroPlayerLogger.log("TrackPlayerCore",
124
+ "✅ Track updates complete - \(affectedPlaylists.count) playlists affected")
125
+ }
126
+
127
+ func getTracksNeedingUrlsInternal() -> [TrackItem] {
128
+ guard let currentId = currentPlaylistId,
129
+ let playlist = playlistManager.getPlaylist(playlistId: currentId)
130
+ else { return [] }
131
+
132
+ // Only return tracks that truly can't play: empty remote URL AND not downloaded locally.
133
+ return playlist.tracks.filter {
134
+ $0.url.isEmpty && !DownloadManagerCore.shared.isTrackDownloaded(trackId: $0.id)
135
+ }
136
+ }
137
+
138
+ func getNextTracksInternal(count: Int) -> [TrackItem] {
139
+ let actualQueue = getActualQueueInternal()
140
+ guard !actualQueue.isEmpty else { return [] }
141
+
142
+ guard let currentTrack = getCurrentTrack(),
143
+ let currentIndex = actualQueue.firstIndex(where: { $0.id == currentTrack.id })
144
+ else { return [] }
145
+
146
+ let startIndex = currentIndex + 1
147
+ let endIndex = min(startIndex + count, actualQueue.count)
148
+ return startIndex < actualQueue.count ? Array(actualQueue[startIndex..<endIndex]) : []
149
+ }
150
+
151
+ func checkUpcomingTracksForUrls(lookahead: Int = 5) {
152
+ let upcomingTracks = getNextTracksInternal(count: lookahead)
153
+
154
+ let currentTrack = getCurrentTrack()
155
+ let currentNeedsUrl = currentTrack.map {
156
+ $0.url.isEmpty && !DownloadManagerCore.shared.isTrackDownloaded(trackId: $0.id)
157
+ } ?? false
158
+ let candidateTracks = currentNeedsUrl ? [currentTrack!] + upcomingTracks : upcomingTracks
159
+
160
+ let tracksNeedingUrls = candidateTracks.filter {
161
+ $0.url.isEmpty && !DownloadManagerCore.shared.isTrackDownloaded(trackId: $0.id)
162
+ }
163
+
164
+ if !tracksNeedingUrls.isEmpty {
165
+ NitroPlayerLogger.log("TrackPlayerCore", "⚠️ \(tracksNeedingUrls.count) upcoming tracks need URLs")
166
+ notifyTracksNeedUpdate(tracks: tracksNeedingUrls, lookahead: lookahead)
167
+ }
168
+ }
169
+ }
@@ -22,12 +22,12 @@ class EqualizerCore {
22
22
  private(set) var isEqualizerEnabled: Bool = false
23
23
  private var currentPresetName: String?
24
24
 
25
- // Standard 5-band frequencies: 60Hz, 230Hz, 910Hz, 3.6kHz, 14kHz
26
- let frequencies: [Float] = [60, 230, 910, 3600, 14000]
27
- private let frequencyLabels = ["60 Hz", "230 Hz", "910 Hz", "3.6 kHz", "14 kHz"]
25
+ // Standard 10-band frequencies: 31Hz, 63Hz, 125Hz, 250Hz, 500Hz, 1kHz, 2kHz, 4kHz, 8kHz, 16kHz
26
+ let frequencies: [Float] = [31, 63, 125, 250, 500, 1000, 2000, 4000, 8000, 16000]
27
+ private let frequencyLabels = ["31 Hz", "63 Hz", "125 Hz", "250 Hz", "500 Hz", "1 kHz", "2 kHz", "4 kHz", "8 kHz", "16 kHz"]
28
28
 
29
29
  // Current gains storage - internal so TapContext can access
30
- private(set) var currentGains: [Double] = [0, 0, 0, 0, 0]
30
+ private(set) var currentGains: [Double] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
31
31
  private var cachedBands: [EqualizerBand]?
32
32
  private var cachedCustomPresets: [EqualizerPreset]?
33
33
 
@@ -40,46 +40,33 @@ class EqualizerCore {
40
40
  private let currentPresetKey = "eq_current_preset"
41
41
  private let customPresetsKey = "eq_custom_presets"
42
42
 
43
- // MARK: - Weak Callback Wrapper
44
-
45
- private class WeakCallbackBox<T> {
46
- private(set) weak var owner: AnyObject?
47
- let callback: T
48
-
49
- init(owner: AnyObject, callback: T) {
50
- self.owner = owner
51
- self.callback = callback
52
- }
53
-
54
- var isAlive: Bool { owner != nil }
55
- }
56
-
57
- // Event callbacks
58
- private var onEnabledChangeListeners: [WeakCallbackBox<(Bool) -> Void>] = []
59
- private var onBandChangeListeners: [WeakCallbackBox<([EqualizerBand]) -> Void>] = []
60
- private var onPresetChangeListeners: [WeakCallbackBox<(Variant_NullType_String?) -> Void>] = []
61
-
62
- private let listenersQueue = DispatchQueue(
63
- label: "com.equalizer.listeners", attributes: .concurrent)
43
+ // Event listeners (v2 ListenerRegistry with stable IDs)
44
+ private let onEnabledChangeListeners = ListenerRegistry<(Bool) -> Void>()
45
+ private let onBandChangeListeners = ListenerRegistry<([EqualizerBand]) -> Void>()
46
+ private let onPresetChangeListeners = ListenerRegistry<(Variant_NullType_String?) -> Void>()
64
47
 
65
48
  // MARK: - Built-in Presets
66
49
 
67
50
  private static let builtInPresets: [String: [Double]] = [
68
- "Flat": [0, 0, 0, 0, 0],
69
- "Bass Boost": [6, 4, 0, 0, 0],
70
- "Bass Reducer": [-6, -4, 0, 0, 0],
71
- "Treble Boost": [0, 0, 0, 4, 6],
72
- "Treble Reducer": [0, 0, 0, -4, -6],
73
- "Vocal Boost": [-2, 0, 4, 2, 0],
74
- "Rock": [5, 3, -1, 3, 5],
75
- "Pop": [-1, 2, 4, 2, -1],
76
- "Jazz": [3, 1, -2, 2, 4],
77
- "Classical": [4, 2, -1, 2, 3],
78
- "Hip Hop": [6, 4, 0, 1, 3],
79
- "Electronic": [5, 3, 0, 2, 5],
80
- "Acoustic": [4, 2, 1, 3, 3],
81
- "R&B": [3, 6, 2, -1, 2],
82
- "Loudness": [6, 3, -1, 3, 6],
51
+ "Flat": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
52
+ "Rock": [4.8, 2.88, -3.36, -4.8, -1.92, 2.4, 5.28, 6.72, 6.72, 6.72],
53
+ "Pop": [0.96, 2.88, 4.32, 4.8, 3.36, 0.0, -1.44, -1.44, 0.96, 0.96],
54
+ "Classical": [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -4.32, -4.32, -4.32, -5.76],
55
+ "Dance": [5.76, 4.32, 1.44, 0.0, 0.0, -3.36, -4.32, -4.32, 0.0, 0.0],
56
+ "Techno": [4.8, 3.36, 0.0, -3.36, -2.88, 0.0, 4.8, 5.76, 5.76, 5.28],
57
+ "Club": [0.0, 0.0, 4.8, 3.36, 3.36, 3.36, 1.92, 0.0, 0.0, 0.0],
58
+ "Live": [-2.88, 0.0, 2.4, 3.36, 3.36, 3.36, 2.4, 1.44, 1.44, 1.44],
59
+ "Reggae": [0.0, 0.0, 0.0, -3.36, 0.0, 3.84, 3.84, 0.0, 0.0, 0.0],
60
+ "Full Bass": [4.8, 5.76, 5.76, 3.36, 0.96, -2.4, -4.8, -6.24, -6.72, -6.72],
61
+ "Full Treble": [-5.76, -5.76, -5.76, -2.4, 1.44, 6.72, 9.6, 9.6, 9.6, 10.08],
62
+ "Full Bass & Treble": [4.32, 3.36, 0.0, -4.32, -2.88, 0.96, 4.8, 6.72, 7.2, 7.2],
63
+ "Large Hall": [6.24, 6.24, 3.36, 3.36, 0.0, -2.88, -2.88, -2.88, 0.0, 0.0],
64
+ "Party": [4.32, 4.32, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 4.32, 4.32],
65
+ "Ska": [-1.44, -2.88, -2.4, 0.0, 2.4, 3.36, 5.28, 5.76, 6.72, 5.76],
66
+ "Soft": [2.88, 0.96, 0.0, -1.44, 0.0, 2.4, 4.8, 5.76, 6.72, 7.2],
67
+ "Soft Rock": [2.4, 2.4, 1.44, 0.0, -2.4, -3.36, -1.92, 0.0, 1.44, 5.28],
68
+ "Headphones": [2.88, 6.72, 3.36, -1.92, -1.44, 0.96, 2.88, 5.76, 7.68, 8.64],
69
+ "Laptop Speakers": [2.88, 6.72, 3.36, -1.92, -1.44, 0.96, 2.88, 5.76, 7.68, 8.64],
83
70
  ]
84
71
 
85
72
  // MARK: - Initialization
@@ -206,7 +193,7 @@ class EqualizerCore {
206
193
 
207
194
  func getBands() -> [EqualizerBand] {
208
195
  if let cached = cachedBands { return cached }
209
- let bands = (0..<5).map { i in
196
+ let bands = (0..<10).map { i in
210
197
  EqualizerBand(
211
198
  index: Double(i),
212
199
  centerFrequency: Double(frequencies[i]),
@@ -219,7 +206,7 @@ class EqualizerCore {
219
206
  }
220
207
 
221
208
  func setBandGain(bandIndex: Int, gainDb: Double) -> Bool {
222
- guard bandIndex >= 0 && bandIndex < 5 else { return false }
209
+ guard bandIndex >= 0 && bandIndex < 10 else { return false }
223
210
 
224
211
  let clampedGain = max(-12.0, min(12.0, gainDb))
225
212
  currentGains[bandIndex] = clampedGain
@@ -237,9 +224,9 @@ class EqualizerCore {
237
224
  }
238
225
 
239
226
  func setAllBandGains(_ gains: [Double]) -> Bool {
240
- guard gains.count == 5 else { return false }
227
+ guard gains.count == 10 else { return false }
241
228
 
242
- for i in 0..<5 {
229
+ for i in 0..<10 {
243
230
  currentGains[i] = max(-12.0, min(12.0, gains[i]))
244
231
  }
245
232
  cachedBands = nil
@@ -390,7 +377,7 @@ class EqualizerCore {
390
377
  }
391
378
 
392
379
  func reset() {
393
- _ = setAllBandGains([0, 0, 0, 0, 0])
380
+ _ = setAllBandGains([0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
394
381
  currentPresetName = "Flat"
395
382
  notifyPresetChange("Flat")
396
383
  saveCurrentPreset("Flat")
@@ -417,10 +404,11 @@ class EqualizerCore {
417
404
 
418
405
  if let data = UserDefaults.standard.data(forKey: bandGainsKey),
419
406
  let gains = try? JSONDecoder().decode([Double].self, from: data),
420
- gains.count == 5
407
+ gains.count == 10
421
408
  {
422
409
  currentGains = gains
423
410
  }
411
+ // else: migration from 5-band or fresh install — start at flat (currentGains already zeroed)
424
412
 
425
413
  currentPresetName = UserDefaults.standard.string(forKey: currentPresetKey)
426
414
  isEqualizerEnabled = enabled
@@ -428,88 +416,40 @@ class EqualizerCore {
428
416
  NitroPlayerLogger.log("EqualizerCore", "✅ Restored settings - enabled: \(enabled), gains: \(currentGains)")
429
417
  }
430
418
 
431
- // MARK: - Callback Management
419
+ // MARK: - Listener Management (v2 — stable IDs)
432
420
 
433
- func addOnEnabledChangeListener(owner: AnyObject, _ callback: @escaping (Bool) -> Void) {
434
- listenersQueue.async(flags: .barrier) { [weak self] in
435
- let box = WeakCallbackBox(owner: owner, callback: callback)
436
- self?.onEnabledChangeListeners.append(box)
437
- }
421
+ @discardableResult func addOnEnabledChangeListener(_ callback: @escaping (Bool) -> Void) -> Int64 {
422
+ onEnabledChangeListeners.add(callback)
423
+ }
424
+ @discardableResult func removeOnEnabledChangeListener(id: Int64) -> Bool {
425
+ onEnabledChangeListeners.remove(id: id)
438
426
  }
439
427
 
440
- func addOnBandChangeListener(owner: AnyObject, _ callback: @escaping ([EqualizerBand]) -> Void) {
441
- listenersQueue.async(flags: .barrier) { [weak self] in
442
- let box = WeakCallbackBox(owner: owner, callback: callback)
443
- self?.onBandChangeListeners.append(box)
444
- }
428
+ @discardableResult func addOnBandChangeListener(_ callback: @escaping ([EqualizerBand]) -> Void) -> Int64 {
429
+ onBandChangeListeners.add(callback)
430
+ }
431
+ @discardableResult func removeOnBandChangeListener(id: Int64) -> Bool {
432
+ onBandChangeListeners.remove(id: id)
445
433
  }
446
434
 
447
- func addOnPresetChangeListener(
448
- owner: AnyObject, _ callback: @escaping (Variant_NullType_String?) -> Void
449
- ) {
450
- listenersQueue.async(flags: .barrier) { [weak self] in
451
- let box = WeakCallbackBox(owner: owner, callback: callback)
452
- self?.onPresetChangeListeners.append(box)
453
- }
435
+ @discardableResult func addOnPresetChangeListener(_ callback: @escaping (Variant_NullType_String?) -> Void) -> Int64 {
436
+ onPresetChangeListeners.add(callback)
437
+ }
438
+ @discardableResult func removeOnPresetChangeListener(id: Int64) -> Bool {
439
+ onPresetChangeListeners.remove(id: id)
454
440
  }
455
441
 
456
442
  private func notifyEnabledChange(_ enabled: Bool) {
457
- listenersQueue.async(flags: .barrier) { [weak self] in
458
- guard let self = self else { return }
459
- self.onEnabledChangeListeners.removeAll { !$0.isAlive }
460
-
461
- let callbacks = self.onEnabledChangeListeners.compactMap {
462
- $0.isAlive ? $0.callback : nil
463
- }
464
-
465
- if !callbacks.isEmpty {
466
- DispatchQueue.main.async {
467
- for callback in callbacks {
468
- callback(enabled)
469
- }
470
- }
471
- }
472
- }
443
+ onEnabledChangeListeners.forEach { $0(enabled) }
473
444
  }
474
445
 
475
446
  private func notifyBandChange(_ bands: [EqualizerBand]) {
476
- listenersQueue.async(flags: .barrier) { [weak self] in
477
- guard let self = self else { return }
478
- self.onBandChangeListeners.removeAll { !$0.isAlive }
479
-
480
- let callbacks = self.onBandChangeListeners.compactMap {
481
- $0.isAlive ? $0.callback : nil
482
- }
483
-
484
- if !callbacks.isEmpty {
485
- DispatchQueue.main.async {
486
- for callback in callbacks {
487
- callback(bands)
488
- }
489
- }
490
- }
491
- }
447
+ onBandChangeListeners.forEach { $0(bands) }
492
448
  }
493
449
 
494
450
  private func notifyPresetChange(_ presetName: String?) {
495
- listenersQueue.async(flags: .barrier) { [weak self] in
496
- guard let self = self else { return }
497
- self.onPresetChangeListeners.removeAll { !$0.isAlive }
498
-
499
- let callbacks = self.onPresetChangeListeners.compactMap {
500
- $0.isAlive ? $0.callback : nil
501
- }
502
-
503
- if !callbacks.isEmpty {
504
- let variant: Variant_NullType_String? = presetName.map { .second($0) }
505
-
506
- DispatchQueue.main.async {
507
- for callback in callbacks {
508
- callback(variant)
509
- }
510
- }
511
- }
512
- }
451
+ let variant: Variant_NullType_String? = presetName.map { .second($0) }
452
+ onPresetChangeListeners.forEach { $0(variant) }
513
453
  }
514
454
  }
515
455
 
@@ -521,28 +461,28 @@ private class TapContext {
521
461
  var sampleRate: Float = 44100.0
522
462
  var channelCount: Int = 2
523
463
 
524
- // Biquad filter states for 5 bands
464
+ // Biquad filter states for 10 bands
525
465
  // Each band needs 4 delay elements per channel (x[n-1], x[n-2], y[n-1], y[n-2])
526
466
  var filterStates: [[Float]] = []
527
467
 
528
- // Biquad coefficients for 5 bands
468
+ // Biquad coefficients for 10 bands
529
469
  // Each band: [b0, b1, b2, a1, a2] (normalized, a0 = 1)
530
470
  var filterCoeffs: [[Double]] = []
531
471
 
532
472
  init(eqCore: EqualizerCore) {
533
473
  self.eqCore = eqCore
534
- // Initialize 5 bands with flat coefficients
535
- for _ in 0..<5 {
474
+ // Initialize 10 bands with flat coefficients
475
+ for _ in 0..<10 {
536
476
  filterCoeffs.append([1.0, 0.0, 0.0, 0.0, 0.0]) // Flat/bypass
537
477
  }
538
478
  }
539
479
 
540
480
  func updateCoefficients() {
541
481
  guard let eqCore = eqCore else { return }
542
- let frequencies: [Float] = [60, 230, 910, 3600, 14000]
482
+ let frequencies: [Float] = [31, 63, 125, 250, 500, 1000, 2000, 4000, 8000, 16000]
543
483
  let gains = eqCore.currentGains
544
484
 
545
- for i in 0..<5 {
485
+ for i in 0..<10 {
546
486
  filterCoeffs[i] = calculatePeakingEQCoefficients(
547
487
  frequency: Double(frequencies[i]),
548
488
  gain: gains[i],
@@ -582,7 +522,7 @@ private class TapContext {
582
522
 
583
523
  func resetFilterStates() {
584
524
  filterStates = []
585
- for _ in 0..<5 {
525
+ for _ in 0..<10 {
586
526
  // 4 delay elements per channel (2 for input history, 2 for output history)
587
527
  filterStates.append(Array(repeating: Float(0.0), count: channelCount * 4))
588
528
  }
@@ -678,8 +618,8 @@ private func tapProcessCallback(
678
618
  let frameCount = Int(numberFramesOut.pointee)
679
619
  let samples = data.assumingMemoryBound(to: Float.self)
680
620
 
681
- // Apply all 5 EQ bands in series
682
- for bandIndex in 0..<5 {
621
+ // Apply all 10 EQ bands in series
622
+ for bandIndex in 0..<10 {
683
623
  let coeffs: [Double] = context.filterCoeffs[bandIndex]
684
624
 
685
625
  // Skip if essentially flat
@@ -20,7 +20,7 @@ class MediaSessionManager {
20
20
 
21
21
  // MARK: - Properties
22
22
 
23
- private var trackPlayerCore: TrackPlayerCore?
23
+ private weak var trackPlayerCore: TrackPlayerCore?
24
24
  private let artworkCache = NSCache<NSString, UIImage>()
25
25
 
26
26
  private var showInNotification: Bool = true
@@ -28,6 +28,11 @@ class MediaSessionManager {
28
28
  // Tracks the artwork URL currently shown so we can discard stale async loads
29
29
  private var lastArtworkUrl: String?
30
30
 
31
+ // Cached values received from playerQueue — main-thread-only reads (no sync needed)
32
+ private var cachedTrack: TrackItem?
33
+ private var cachedState: PlayerState?
34
+ private var cachedQueue: [TrackItem] = []
35
+
31
36
  init() {
32
37
  setupRemoteCommandCenter()
33
38
  }
@@ -47,27 +52,27 @@ class MediaSessionManager {
47
52
  refresh()
48
53
  }
49
54
 
50
- // MARK: - Single refresh entry point
55
+ // MARK: - Entry point from playerQueue (called via DispatchQueue.main.async)
51
56
  //
52
- // All public callbacks route here. Always dispatches to main thread so
53
- // MPNowPlayingInfoCenter and MPRemoteCommandCenter are only touched from main.
57
+ // Receives pre-computed values captured on playerQueue no player access here.
58
+
59
+ func updateFromPlayerQueue(track: TrackItem, state: PlayerState, queue: [TrackItem]) {
60
+ cachedTrack = track
61
+ cachedState = state
62
+ cachedQueue = queue
63
+ refreshInternal()
64
+ }
65
+
66
+ // MARK: - Refresh using cached values (main thread only)
54
67
 
55
68
  func refresh() {
56
69
  if Thread.isMainThread {
57
70
  refreshInternal()
58
71
  } else {
59
- DispatchQueue.main.async { [weak self] in
60
- self?.refreshInternal()
61
- }
72
+ DispatchQueue.main.async { [weak self] in self?.refreshInternal() }
62
73
  }
63
74
  }
64
75
 
65
- // Convenience aliases used by TrackPlayerCore call sites
66
- func updateNowPlayingInfo() { refresh() }
67
- func onTrackChanged() { refresh() }
68
- func onPlaybackStateChanged() { refresh() }
69
- func onQueueChanged() { refresh() }
70
-
71
76
  // MARK: - Core internal update (main thread only)
72
77
 
73
78
  private func refreshInternal() {
@@ -77,21 +82,13 @@ class MediaSessionManager {
77
82
  return
78
83
  }
79
84
 
80
- guard let core = trackPlayerCore,
81
- let track = core.getCurrentTrack()
82
- else {
85
+ guard let track = cachedTrack, let state = cachedState else {
83
86
  clearNowPlayingInfo()
84
87
  disableAllCommands()
85
88
  return
86
89
  }
87
90
 
88
- // Fetch snapshot once — both calls are cheap on main thread (no sync overhead)
89
- let state = core.getState()
90
- let queue = core.getActualQueue()
91
-
92
- // Find the actual position of the current track inside the actual queue.
93
- // state.currentIndex is the original-playlist index which is wrong when a
94
- // temp (playNext / upNext) track is playing.
91
+ let queue = cachedQueue
95
92
  let positionInQueue = queue.firstIndex(where: { $0.id == track.id }) ?? -1
96
93
 
97
94
  updateNowPlayingInfoInternal(track: track, state: state, queue: queue, positionInQueue: positionInQueue)
@@ -145,7 +142,6 @@ class MediaSessionManager {
145
142
  MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
146
143
  loadArtwork(url: artworkUrl) { [weak self] image in
147
144
  guard let self = self, let image = image else { return }
148
- // Discard if track changed while loading
149
145
  guard self.lastArtworkUrl == artworkUrl else { return }
150
146
  var updated = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [:]
151
147
  updated[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(
@@ -168,8 +164,6 @@ class MediaSessionManager {
168
164
  private func setupRemoteCommandCenter() {
169
165
  let commandCenter = MPRemoteCommandCenter.shared()
170
166
 
171
- // Clear any previously registered targets before adding fresh ones.
172
- // Prevents duplicate handlers if this were ever called more than once.
173
167
  commandCenter.playCommand.removeTarget(nil)
174
168
  commandCenter.pauseCommand.removeTarget(nil)
175
169
  commandCenter.togglePlayPauseCommand.removeTarget(nil)
@@ -183,7 +177,7 @@ class MediaSessionManager {
183
177
  commandCenter.playCommand.isEnabled = true
184
178
  commandCenter.playCommand.addTarget { [weak self] _ in
185
179
  guard let core = self?.trackPlayerCore else { return .commandFailed }
186
- core.play()
180
+ Task { await core.play() }
187
181
  return .success
188
182
  }
189
183
 
@@ -191,7 +185,7 @@ class MediaSessionManager {
191
185
  commandCenter.pauseCommand.isEnabled = true
192
186
  commandCenter.pauseCommand.addTarget { [weak self] _ in
193
187
  guard let core = self?.trackPlayerCore else { return .commandFailed }
194
- core.pause()
188
+ Task { await core.pause() }
195
189
  return .success
196
190
  }
197
191
 
@@ -199,51 +193,45 @@ class MediaSessionManager {
199
193
  commandCenter.togglePlayPauseCommand.isEnabled = true
200
194
  commandCenter.togglePlayPauseCommand.addTarget { [weak self] _ in
201
195
  guard let core = self?.trackPlayerCore else { return .commandFailed }
202
- if core.getState().currentState == .playing {
203
- core.pause()
204
- } else {
205
- core.play()
206
- }
196
+ let isPlaying = self?.cachedState?.currentState == .playing
197
+ Task { if isPlaying { await core.pause() } else { await core.play() } }
207
198
  return .success
208
199
  }
209
200
 
210
- // Next track — isEnabled managed dynamically in updateCommandCenterState
201
+ // Next track
211
202
  commandCenter.nextTrackCommand.isEnabled = false
212
203
  commandCenter.nextTrackCommand.addTarget { [weak self] _ in
213
204
  guard let core = self?.trackPlayerCore else { return .commandFailed }
214
- core.skipToNext()
205
+ Task { await core.skipToNext() }
215
206
  return .success
216
207
  }
217
208
 
218
- // Previous track — isEnabled managed dynamically in updateCommandCenterState
209
+ // Previous track
219
210
  commandCenter.previousTrackCommand.isEnabled = false
220
211
  commandCenter.previousTrackCommand.addTarget { [weak self] _ in
221
212
  guard let core = self?.trackPlayerCore else { return .commandFailed }
222
- core.skipToPrevious()
213
+ Task { await core.skipToPrevious() }
223
214
  return .success
224
215
  }
225
216
 
226
- // Disable skip-forward/backward — these replace the scrubber with non-interactive buttons
227
217
  commandCenter.seekForwardCommand.isEnabled = false
228
218
  commandCenter.seekBackwardCommand.isEnabled = false
229
219
 
230
- // Scrubber — isEnabled managed dynamically based on known duration
220
+ // Scrubber
231
221
  commandCenter.changePlaybackPositionCommand.isEnabled = false
232
222
  commandCenter.changePlaybackPositionCommand.addTarget { [weak self] event in
233
- guard let self = self,
234
- let core = self.trackPlayerCore,
223
+ guard let core = self?.trackPlayerCore,
235
224
  let positionEvent = event as? MPChangePlaybackPositionCommandEvent
236
225
  else {
237
226
  return .commandFailed
238
227
  }
239
- // Optimistically freeze the scrubber at the tapped position while the async
240
- // seek is in flight — updateNowPlayingInfo in the seek completion restores it.
228
+ // Optimistically freeze the scrubber at the tapped position
241
229
  if var info = MPNowPlayingInfoCenter.default().nowPlayingInfo {
242
230
  info[MPNowPlayingInfoPropertyElapsedPlaybackTime] = positionEvent.positionTime
243
231
  info[MPNowPlayingInfoPropertyPlaybackRate] = 0.0
244
232
  MPNowPlayingInfoCenter.default().nowPlayingInfo = info
245
233
  }
246
- core.seek(position: positionEvent.positionTime)
234
+ Task { await core.seek(position: positionEvent.positionTime) }
247
235
  return .success
248
236
  }
249
237
  }
@@ -260,13 +248,8 @@ class MediaSessionManager {
260
248
  let playerDuration = state.totalDuration
261
249
  let hasDuration = playerDuration > 0 && !playerDuration.isNaN && !playerDuration.isInfinite
262
250
 
263
- // Next: only enabled when there is a track after the current one
264
251
  commandCenter.nextTrackCommand.isEnabled = hasCurrentTrack && isNotLast
265
-
266
- // Previous: always enabled when something is playing — either restarts current or goes back
267
252
  commandCenter.previousTrackCommand.isEnabled = hasCurrentTrack
268
-
269
- // Scrubber: only enabled when we have a known, finite duration
270
253
  commandCenter.changePlaybackPositionCommand.isEnabled = hasCurrentTrack && hasDuration
271
254
  }
272
255