react-native-queue-player 1.2.0 → 2.0.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 (362) hide show
  1. package/QueuePlayer.podspec +14 -9
  2. package/README.md +3 -3
  3. package/android/consumer-rules.pro +1 -1
  4. package/android/src/main/cpp/airplay2_control.c +4 -4
  5. package/android/src/main/cpp/airplay2_control.h +4 -4
  6. package/android/src/main/cpp/airplay2_jni.cpp +3 -19
  7. package/android/src/main/cpp/airplay2_pair.c +7 -7
  8. package/android/src/main/cpp/airplay2_pair.h +2 -2
  9. package/android/src/main/cpp/airplay2_rtsp.c +8 -8
  10. package/android/src/main/cpp/airplay2_session.c +2 -2
  11. package/android/src/main/cpp/airplay_jni.cpp +8 -69
  12. package/android/src/main/java/com/margelo/nitro/queueplayer/AirPlayEngine.kt +7 -16
  13. package/android/src/main/java/com/margelo/nitro/queueplayer/CacheMimeTypes.kt +1 -1
  14. package/android/src/main/java/com/margelo/nitro/queueplayer/CastManager.kt +2 -14
  15. package/android/src/main/java/com/margelo/nitro/queueplayer/CellularTransportMonitor.kt +77 -0
  16. package/android/src/main/java/com/margelo/nitro/queueplayer/CrossfadeEngine.kt +187 -52
  17. package/android/src/main/java/com/margelo/nitro/queueplayer/Equalizer.kt +27 -7
  18. package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerEngine.kt +33 -12
  19. package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngine.kt +31 -8
  20. package/android/src/main/java/com/margelo/nitro/queueplayer/FFTProcessorTee.kt +45 -30
  21. package/android/src/main/java/com/margelo/nitro/queueplayer/FifoCacheEvictor.kt +111 -15
  22. package/android/src/main/java/com/margelo/nitro/queueplayer/GaplessEngine.kt +60 -33
  23. package/android/src/main/java/com/margelo/nitro/queueplayer/HeadlessJsMediaService.kt +1 -1
  24. package/android/src/main/java/com/margelo/nitro/queueplayer/IEqualizerEngine.kt +3 -1
  25. package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCache.kt +111 -29
  26. package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCacheWriter.kt +78 -30
  27. package/android/src/main/java/com/margelo/nitro/queueplayer/MediaItemBuilder.kt +6 -6
  28. package/android/src/main/java/com/margelo/nitro/queueplayer/MediaResponseCheck.kt +156 -0
  29. package/android/src/main/java/com/margelo/nitro/queueplayer/MediaValidatingDataSource.kt +165 -0
  30. package/android/src/main/java/com/margelo/nitro/queueplayer/MimeCapturingDataSource.kt +18 -9
  31. package/android/src/main/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractor.kt +48 -1
  32. package/android/src/main/java/com/margelo/nitro/queueplayer/PitchCorrection.kt +3 -2
  33. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackEngine.kt +71 -76
  34. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackErrorMapping.kt +13 -5
  35. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackModeStateMachine.kt +2 -2
  36. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackService.kt +68 -46
  37. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackServiceCallback.kt +6 -5
  38. package/android/src/main/java/com/margelo/nitro/queueplayer/QueueMutationArithmetic.kt +5 -6
  39. package/android/src/main/java/com/margelo/nitro/queueplayer/QueueSkipArithmetic.kt +11 -8
  40. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessor.kt +26 -6
  41. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainData.kt +22 -3
  42. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainExtractor.kt +38 -12
  43. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainGain.kt +17 -13
  44. package/android/src/main/java/com/margelo/nitro/queueplayer/SuppliedReplayGain.kt +66 -0
  45. package/android/src/main/java/com/margelo/nitro/queueplayer/TrackPlayer.kt +985 -276
  46. package/android/src/main/java/com/margelo/nitro/queueplayer/Visualizer.kt +4 -8
  47. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastBackend.kt +4 -3
  48. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastSession.kt +5 -4
  49. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2JNI.kt +0 -2
  50. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2Session.kt +4 -2
  51. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayBackend.kt +16 -9
  52. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayMetadataSync.kt +1 -1
  53. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayRenderersFactory.kt +6 -14
  54. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirplayJNI.kt +1 -13
  55. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/MulticastLockHolder.kt +1 -1
  56. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/chromecast/ChromecastBackend.kt +3 -3
  57. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServer.kt +11 -35
  58. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaServerHandle.kt +0 -5
  59. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaTokenRegistry.kt +0 -6
  60. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitor.kt +32 -33
  61. package/android/src/test/java/androidx/media3/session/{MediaSessionControllerRequestTestSeam.kt → MediaSessionControllerRequestTestSupport.kt} +1 -1
  62. package/android/src/test/java/com/margelo/nitro/queueplayer/AvrcpMetadataTest.kt +12 -0
  63. package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineFocusLossTest.kt +115 -0
  64. package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineLifecycleTest.kt +45 -0
  65. package/android/src/test/java/com/margelo/nitro/queueplayer/EngineEndSignalOrderTest.kt +70 -0
  66. package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerEngineTest.kt +17 -0
  67. package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerKtTest.kt +0 -6
  68. package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngineTest.kt +17 -0
  69. package/android/src/test/java/com/margelo/nitro/queueplayer/FFTProcessorTeeTest.kt +15 -0
  70. package/android/src/test/java/com/margelo/nitro/queueplayer/FifoCacheEvictorTest.kt +135 -4
  71. package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineLifecycleTest.kt +1 -0
  72. package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineReplayGainTest.kt +36 -2
  73. package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheTest.kt +84 -18
  74. package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterCancelTest.kt +85 -0
  75. package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterTest.kt +112 -1
  76. package/android/src/test/java/com/margelo/nitro/queueplayer/MediaItemBuilderTest.kt +5 -3
  77. package/android/src/test/java/com/margelo/nitro/queueplayer/MediaResponseCheckTest.kt +163 -0
  78. package/android/src/test/java/com/margelo/nitro/queueplayer/MediaValidatingDataSourceTest.kt +189 -0
  79. package/android/src/test/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractorTest.kt +45 -0
  80. package/android/src/test/java/com/margelo/nitro/queueplayer/PitchAwareAudioProcessorChainTest.kt +1 -1
  81. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackSearchTest.kt +47 -0
  82. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackTest.kt +53 -29
  83. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceLifecycleTest.kt +8 -8
  84. package/android/src/test/java/com/margelo/nitro/queueplayer/QueueSkipArithmeticTest.kt +33 -1
  85. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessorTest.kt +128 -5
  86. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainExtractorTest.kt +209 -14
  87. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainGainTest.kt +150 -10
  88. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainMergeTest.kt +258 -0
  89. package/android/src/test/java/com/margelo/nitro/queueplayer/RobolectricServiceBindHelper.kt +2 -2
  90. package/android/src/test/java/com/margelo/nitro/queueplayer/SessionCommandForwardingPlayerTest.kt +2 -2
  91. package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowDynamicsProcessingRejectingEnable.kt +19 -0
  92. package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizer.kt +1 -1
  93. package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizerRejectingBandWrites.kt +27 -0
  94. package/android/src/test/java/com/margelo/nitro/queueplayer/SuppliedReplayGainTest.kt +61 -0
  95. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAirPlayMetadataWiringTest.kt +4 -4
  96. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAudioFocusTest.kt +1 -1
  97. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastCommandRoutingTest.kt +2 -1
  98. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastStateTest.kt +2 -1
  99. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerEventsTest.kt +122 -38
  100. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLifecycleTest.kt +291 -27
  101. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLookaheadConfigTest.kt +277 -38
  102. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerMutationTest.kt +5 -3
  103. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerProgressThrottleTest.kt +2 -1
  104. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueChangeTest.kt +85 -3
  105. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueTest.kt +11 -7
  106. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerReadersTest.kt +2 -1
  107. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipCapabilityTest.kt +90 -4
  108. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipTest.kt +8 -26
  109. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerTransportTest.kt +3 -3
  110. package/android/src/test/java/com/margelo/nitro/queueplayer/VisualizerKtTest.kt +0 -4
  111. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/FakeRemotePlayer.kt +2 -2
  112. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitorTest.kt +173 -0
  113. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/ShadowConnectivityManagerRecordingRequests.kt +27 -0
  114. package/app.plugin.js +17 -1
  115. package/ios/AVPlayerItemQueueItemId.swift +26 -12
  116. package/ios/AVQueueBuilder.swift +120 -85
  117. package/ios/ArtworkLoader.swift +0 -7
  118. package/ios/ArtworkResolver.swift +22 -4
  119. package/ios/AssetReplayGainReader.swift +118 -0
  120. package/ios/AudioSession.swift +67 -16
  121. package/ios/AudioTapProvider.swift +284 -149
  122. package/ios/CarPlayBridge.swift +0 -12
  123. package/ios/CarPlayCoordinator.swift +18 -2
  124. package/ios/CarPlaySceneDelegate.swift +2 -2
  125. package/ios/Cast/AirPlayRouteState.swift +9 -8
  126. package/ios/Cast/Core/CastEventBridge.swift +66 -26
  127. package/ios/Cast/Core/CastNowPlayingController.swift +38 -16
  128. package/ios/Cast/Core/CastSession.swift +2 -2
  129. package/ios/Cast/Core/CastTransportRouter.swift +21 -9
  130. package/ios/Cast/Core/LocalMediaServer.swift +41 -52
  131. package/ios/Cast/Core/LocalNetworkPermissionProbe.swift +32 -11
  132. package/ios/Cast/Core/MediaServerHandle.swift +0 -4
  133. package/ios/Cast/Core/MediaTokenRegistry.swift +0 -6
  134. package/ios/CastManager.swift +1 -1
  135. package/ios/CrossfadeEngine.swift +672 -226
  136. package/ios/EQTap.swift +20 -17
  137. package/ios/Equalizer.swift +1 -1
  138. package/ios/FLACStreamInfo.swift +65 -0
  139. package/ios/GaplessEngine.swift +57 -30
  140. package/ios/InputGuards.swift +35 -5
  141. package/ios/LookaheadCache.swift +229 -58
  142. package/ios/LookaheadCachePrefetcher.swift +48 -36
  143. package/ios/MediaResponseCheck.swift +122 -0
  144. package/ios/MediaServer/MediaHTTPConnection.swift +691 -0
  145. package/ios/NetworkRecoveryPolicy.swift +32 -0
  146. package/ios/NowPlayingFormatExtractor.swift +25 -18
  147. package/ios/NowPlayingInfo.swift +84 -48
  148. package/ios/OutputRouteMonitor.swift +16 -3
  149. package/ios/PendingBrowseRequests.swift +1 -1
  150. package/ios/PlaceholderArtwork.swift +9 -5
  151. package/ios/PlaybackEngine.swift +165 -76
  152. package/ios/PlaybackErrorMapping.swift +3 -3
  153. package/ios/PlaybackModeStateMachine.swift +2 -2
  154. package/ios/PlaybackNetworkMonitor.swift +73 -0
  155. package/ios/PlayerStateDerivation.swift +6 -6
  156. package/ios/QueueMutationArithmetic.swift +25 -0
  157. package/ios/QueueSkipArithmetic.swift +5 -3
  158. package/ios/QueueWindowArithmetic.swift +152 -0
  159. package/ios/ReadThroughServer.swift +283 -0
  160. package/ios/RemoteCommands.swift +44 -4
  161. package/ios/ReplayGainData.swift +22 -3
  162. package/ios/ReplayGainExtractor.swift +70 -27
  163. package/ios/Siri/VoiceDonation.swift +14 -9
  164. package/ios/StreamingBitrateProbe.swift +22 -68
  165. package/ios/SuppliedReplayGain.swift +83 -0
  166. package/ios/Tests/AVQueueBuilderTests.swift +158 -221
  167. package/ios/Tests/ActiveItemEngineStub.swift +44 -0
  168. package/ios/Tests/AssetReplayGainReaderTests.swift +228 -0
  169. package/ios/Tests/AudioTapProviderDispatchTargetTests.swift +158 -0
  170. package/ios/Tests/AudioTapProviderReplayGainTests.swift +122 -10
  171. package/ios/Tests/BitrateReresolveBudgetTests.swift +71 -0
  172. package/ios/Tests/CastMediaItemTranslationTests.swift +72 -0
  173. package/ios/Tests/CastNowPlayingControllerTests.swift +1 -1
  174. package/ios/Tests/CrossfadeAdvanceWithoutFadeTests.swift +120 -0
  175. package/ios/Tests/CrossfadeEngineIncomingMixTests.swift +122 -0
  176. package/ios/Tests/CrossfadeEngineTests.swift +164 -0
  177. package/ios/Tests/CrossfadeMixReapplyTests.swift +66 -0
  178. package/ios/Tests/CrossfadePlaybackIntentTests.swift +81 -0
  179. package/ios/Tests/EngineSwapTests.swift +91 -0
  180. package/ios/Tests/EqualizerAudioMixProviderTests.swift +46 -0
  181. package/ios/Tests/EqualizerHybridTests.swift +5 -2
  182. package/ios/Tests/FLACStreamInfoTests.swift +88 -0
  183. package/ios/Tests/GaplessEngineLifecycleTests.swift +4 -4
  184. package/ios/Tests/InputGuardsTests.swift +41 -0
  185. package/ios/Tests/InterruptionIntentTests.swift +77 -0
  186. package/ios/Tests/LookaheadCacheCellularAccessTests.swift +185 -0
  187. package/ios/Tests/LookaheadCachePrefetcherTests.swift +53 -30
  188. package/ios/Tests/LookaheadCacheRuntimeConfigTests.swift +160 -1
  189. package/ios/Tests/LookaheadCacheTests.swift +44 -18
  190. package/ios/Tests/MediaHTTPConnectionTests.swift +79 -0
  191. package/ios/Tests/MediaResponseCheckTests.swift +128 -0
  192. package/ios/Tests/MutationDeferralTests.swift +220 -0
  193. package/ios/Tests/NowPlayingInfoTests.swift +86 -99
  194. package/ios/Tests/NowPlayingSnapshotTests.swift +127 -0
  195. package/ios/Tests/OriginRestartStitcherTests.swift +60 -0
  196. package/ios/Tests/PlaybackNetworkMonitorTests.swift +95 -0
  197. package/ios/Tests/PlayerFixtures.swift +48 -0
  198. package/ios/Tests/PlayerStateDerivationTests.swift +10 -10
  199. package/ios/Tests/QueueMutationGenerationTests.swift +141 -0
  200. package/ios/Tests/QueueRebuildPrefixTests.swift +317 -0
  201. package/ios/Tests/QueueStateTests.swift +3 -1
  202. package/ios/Tests/QueueWindowArithmeticTests.swift +56 -0
  203. package/ios/Tests/QueueWindowSliceTests.swift +162 -0
  204. package/ios/Tests/ReadThroughRoutingLifecycleTests.swift +63 -0
  205. package/ios/Tests/ReadThroughServerTests.swift +345 -0
  206. package/ios/Tests/RemoteCommandsTests.swift +28 -0
  207. package/ios/Tests/ReplayGainExtractorTests.swift +216 -6
  208. package/ios/Tests/ReplayGainMergeTests.swift +230 -0
  209. package/ios/Tests/RetryRecoveryTests.swift +418 -0
  210. package/ios/Tests/SkipCapabilityTests.swift +233 -8
  211. package/ios/Tests/SkipIndexTests.swift +32 -2
  212. package/ios/Tests/SleepTimerPauseIntentTests.swift +43 -0
  213. package/ios/Tests/StallRecoveryTests.swift +97 -0
  214. package/ios/Tests/StreamingBitrateProbeTests.swift +17 -20
  215. package/ios/Tests/TopUpWindowGateTests.swift +441 -0
  216. package/ios/Tests/TrackPlayer+TestHops.swift +14 -0
  217. package/ios/Tests/TrackPlayerCallOrderTests.swift +93 -0
  218. package/ios/Tests/TrackPlayerConfigureTeardownTests.swift +86 -0
  219. package/ios/Tests/TrackPlayerEndVerdictTests.swift +206 -0
  220. package/ios/Tests/TrackPlayerSeekTests.swift +137 -0
  221. package/ios/Tests/TrackPlayerThreadingTests.swift +127 -0
  222. package/ios/Tests/TrackSourceClassifierTests.swift +33 -7
  223. package/ios/Tests/VoiceVocabularyOptInTests.swift +38 -0
  224. package/ios/TrackPlayer+Automotive.swift +101 -0
  225. package/ios/TrackPlayer+Cache.swift +232 -0
  226. package/ios/TrackPlayer+Config.swift +291 -0
  227. package/ios/TrackPlayer+EngineDelegate.swift +209 -0
  228. package/ios/TrackPlayer+EventsAPI.swift +111 -0
  229. package/ios/TrackPlayer+EventsDispatch.swift +1042 -0
  230. package/ios/TrackPlayer+EventsWiring.swift +173 -0
  231. package/ios/TrackPlayer+Lifecycle.swift +982 -0
  232. package/ios/TrackPlayer+NowPlayingFormat.swift +258 -0
  233. package/ios/TrackPlayer+Queue.swift +951 -0
  234. package/ios/TrackPlayer+Recovery.swift +213 -0
  235. package/ios/TrackPlayer+Skip.swift +465 -0
  236. package/ios/TrackPlayer+SleepTimer.swift +175 -0
  237. package/ios/TrackPlayer+State.swift +108 -0
  238. package/ios/TrackPlayer+Threading.swift +135 -0
  239. package/ios/TrackPlayer+Transport.swift +272 -0
  240. package/ios/TrackPlayer+Window.swift +227 -0
  241. package/ios/TrackPlayer.swift +331 -4619
  242. package/ios/Visualizer.swift +6 -5
  243. package/ios/tests-harness/Podfile +1 -1
  244. package/ios/tests-harness/TestHost.xcodeproj/project.pbxproj +19 -11
  245. package/ios/tests-harness/scripts/seed-xcodeproj.rb +2 -2
  246. package/lib/module/hooks/useActiveTrack.js +29 -22
  247. package/lib/module/hooks/useActiveTrack.js.map +1 -1
  248. package/lib/module/hooks/useCast.js +8 -26
  249. package/lib/module/hooks/useCast.js.map +1 -1
  250. package/lib/module/hooks/useEqualizer.js +19 -12
  251. package/lib/module/hooks/useEqualizer.js.map +1 -1
  252. package/lib/module/hooks/useLookaheadCache.js +3 -7
  253. package/lib/module/hooks/useLookaheadCache.js.map +1 -1
  254. package/lib/module/hooks/useQueue.js +66 -19
  255. package/lib/module/hooks/useQueue.js.map +1 -1
  256. package/lib/module/index.js +6 -3
  257. package/lib/module/index.js.map +1 -1
  258. package/lib/module/queueDelta.js +41 -0
  259. package/lib/module/queueDelta.js.map +1 -0
  260. package/lib/module/types.js +28 -5
  261. package/lib/module/types.js.map +1 -1
  262. package/lib/typescript/TrackPlayer.nitro.d.ts +53 -28
  263. package/lib/typescript/TrackPlayer.nitro.d.ts.map +1 -1
  264. package/lib/typescript/hooks/useActiveTrack.d.ts +5 -7
  265. package/lib/typescript/hooks/useActiveTrack.d.ts.map +1 -1
  266. package/lib/typescript/hooks/useCast.d.ts +6 -1
  267. package/lib/typescript/hooks/useCast.d.ts.map +1 -1
  268. package/lib/typescript/hooks/useEqualizer.d.ts +2 -1
  269. package/lib/typescript/hooks/useEqualizer.d.ts.map +1 -1
  270. package/lib/typescript/hooks/useLookaheadCache.d.ts.map +1 -1
  271. package/lib/typescript/hooks/useQueue.d.ts +4 -4
  272. package/lib/typescript/hooks/useQueue.d.ts.map +1 -1
  273. package/lib/typescript/index.d.ts +2 -1
  274. package/lib/typescript/index.d.ts.map +1 -1
  275. package/lib/typescript/queueDelta.d.ts +10 -0
  276. package/lib/typescript/queueDelta.d.ts.map +1 -0
  277. package/lib/typescript/types.d.ts +168 -12
  278. package/lib/typescript/types.d.ts.map +1 -1
  279. package/nitrogen/generated/android/c++/JFunc_void_QueueChangeDelta_double_QueueChangeReason.hpp +85 -0
  280. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +45 -19
  281. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +3 -3
  282. package/nitrogen/generated/android/c++/JLookaheadCacheConfig.hpp +8 -4
  283. package/nitrogen/generated/android/c++/JPlayerConfig.hpp +5 -1
  284. package/nitrogen/generated/android/c++/JQueueChangeDelta.hpp +128 -0
  285. package/nitrogen/generated/android/c++/JTrackItem.hpp +19 -3
  286. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_BrowseItem______std__string.kt +0 -2
  287. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_TrackItem______MediaSearchRequest.kt +0 -2
  288. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_TrackItem______std__string.kt +0 -2
  289. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void.kt +0 -2
  290. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_AudioRoute.kt +0 -2
  291. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_BufferState.kt +0 -2
  292. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CacheStatus.kt +0 -2
  293. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastDiscoveryState.kt +0 -2
  294. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastLocalNetworkPermissionEvent.kt +0 -2
  295. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastRoute.kt +0 -2
  296. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastSessionDiedEvent.kt +0 -2
  297. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlaybackError.kt +0 -2
  298. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerProgress.kt +0 -2
  299. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerState_StateChangeReason.kt +0 -2
  300. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/{Func_void_std__vector_TrackItem__double_QueueChangeReason.kt → Func_void_QueueChangeDelta_double_QueueChangeReason.kt} +14 -16
  301. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_ServiceReadyReason.kt +0 -2
  302. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SkipCapability.kt +0 -2
  303. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SleepTimerState.kt +0 -2
  304. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerErrorReason.kt +0 -2
  305. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerFrame.kt +0 -2
  306. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_bool.kt +0 -2
  307. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_double_double.kt +0 -2
  308. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_TrackItem__double_TrackChangeReason_std__optional_double_.kt +0 -2
  309. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_std__variant_nitro__NullType__NowPlayingFormat__.kt +0 -2
  310. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_CastReceiver_.kt +0 -2
  311. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_EqualizerBand_.kt +0 -2
  312. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridCastManagerSpec.kt +2 -0
  313. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridEqualizerSpec.kt +2 -0
  314. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridTrackPlayerSpec.kt +6 -4
  315. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridVisualizerSpec.kt +2 -0
  316. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/LookaheadCacheConfig.kt +9 -4
  317. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/PlayerConfig.kt +7 -2
  318. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/QueueChangeDelta.kt +86 -0
  319. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/TrackItem.kt +24 -4
  320. package/nitrogen/generated/android/queueplayerOnLoad.cpp +2 -2
  321. package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.cpp +16 -16
  322. package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.hpp +80 -65
  323. package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Umbrella.hpp +3 -0
  324. package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +6 -3
  325. package/nitrogen/generated/ios/swift/Func_void_CacheStatus.swift +5 -5
  326. package/nitrogen/generated/ios/swift/Func_void_QueueChangeDelta_double_QueueChangeReason.swift +46 -0
  327. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +3 -3
  328. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +32 -24
  329. package/nitrogen/generated/ios/swift/LookaheadCacheConfig.swift +20 -2
  330. package/nitrogen/generated/ios/swift/PlayerConfig.swift +19 -1
  331. package/nitrogen/generated/ios/swift/QueueChangeDelta.swift +82 -0
  332. package/nitrogen/generated/ios/swift/TrackItem.swift +73 -1
  333. package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +6 -3
  334. package/nitrogen/generated/shared/c++/LookaheadCacheConfig.hpp +7 -3
  335. package/nitrogen/generated/shared/c++/PlayerConfig.hpp +5 -1
  336. package/nitrogen/generated/shared/c++/QueueChangeDelta.hpp +113 -0
  337. package/nitrogen/generated/shared/c++/TrackItem.hpp +18 -2
  338. package/package.json +8 -8
  339. package/src/TrackPlayer.nitro.ts +53 -27
  340. package/src/hooks/useActiveTrack.ts +29 -22
  341. package/src/hooks/useCast.ts +14 -15
  342. package/src/hooks/useEqualizer.ts +19 -12
  343. package/src/hooks/useLookaheadCache.ts +3 -7
  344. package/src/hooks/useQueue.ts +66 -17
  345. package/src/index.ts +13 -3
  346. package/src/queueDelta.ts +41 -0
  347. package/src/types.ts +169 -12
  348. package/android/src/main/java/com/margelo/nitro/queueplayer/PendingIntentBuffer.kt +0 -82
  349. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServerLifecycle.kt +0 -15
  350. package/android/src/test/java/com/margelo/nitro/queueplayer/PendingIntentBufferTest.kt +0 -192
  351. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackEngineInterfaceTest.kt +0 -135
  352. package/android/src/test/java/com/margelo/nitro/queueplayer/SmokeTest.kt +0 -27
  353. package/ios/Cast/Core/LocalAddressMonitor.swift +0 -110
  354. package/ios/Cast/Core/MediaHTTPConnection.swift +0 -349
  355. package/ios/MetadataReader.swift +0 -555
  356. package/ios/Tests/CrossfadeEngineStubTests.swift +0 -43
  357. package/ios/Tests/MetadataReaderTests.swift +0 -603
  358. package/ios/Tests/PlaybackEngineProtocolTests.swift +0 -92
  359. package/ios/Tests/VoiceDonationTests.swift +0 -26
  360. package/nitrogen/generated/android/c++/JFunc_void_std__vector_TrackItem__double_QueueChangeReason.hpp +0 -101
  361. package/nitrogen/generated/ios/swift/Func_void_std__vector_TrackItem__double_QueueChangeReason.swift +0 -46
  362. /package/ios/{Cast/Core → MediaServer}/MimeTypes.swift +0 -0
@@ -36,6 +36,32 @@ final class ReplayGainExtractorTests: XCTestCase {
36
36
  XCTAssertEqual(actual, expected, accuracy: accuracy, file: file, line: line)
37
37
  }
38
38
 
39
+ private func assertAcceptedGain(
40
+ _ value: Double,
41
+ expected: Double,
42
+ accuracy: Double = 0.001,
43
+ file: StaticString = #file, line: UInt = #line
44
+ ) {
45
+ guard let actual = ReplayGainExtractor.acceptGainDb(value) else {
46
+ XCTFail("expected acceptGainDb(\(value)) to succeed", file: file, line: line)
47
+ return
48
+ }
49
+ XCTAssertEqual(actual, expected, accuracy: accuracy, file: file, line: line)
50
+ }
51
+
52
+ private func assertAcceptedPeak(
53
+ _ value: Double,
54
+ expected: Double,
55
+ accuracy: Double = 0.0001,
56
+ file: StaticString = #file, line: UInt = #line
57
+ ) {
58
+ guard let actual = ReplayGainExtractor.acceptPeak(value) else {
59
+ XCTFail("expected acceptPeak(\(value)) to succeed", file: file, line: line)
60
+ return
61
+ }
62
+ XCTAssertEqual(actual, expected, accuracy: accuracy, file: file, line: line)
63
+ }
64
+
39
65
  // MARK: - parseGainDb tolerance
40
66
 
41
67
  func testParseGainDbStandardFormat() {
@@ -94,7 +120,7 @@ final class ReplayGainExtractorTests: XCTestCase {
94
120
  XCTAssertNil(ReplayGainExtractor.parseGainDb("-inf"))
95
121
  }
96
122
 
97
- // MARK: - parsePeak tolerance + encoder-default detection
123
+ // MARK: - parsePeak tolerance + plausible-range guards
98
124
 
99
125
  func testParsePeakStandardLinear() {
100
126
  assertPeak("0.987654", expected: 0.987654)
@@ -102,17 +128,47 @@ final class ReplayGainExtractorTests: XCTestCase {
102
128
  assertPeak("1.234567", expected: 1.234567)
103
129
  }
104
130
 
105
- func testParsePeakRejectsEncoderDefaultsOneAndZero() {
106
- XCTAssertNil(ReplayGainExtractor.parsePeak("1.000000"),
107
- "peak == exactly 1.0 means the analyser didn't run")
108
- XCTAssertNil(ReplayGainExtractor.parsePeak("1"),
109
- "peak == exactly 1.0 means the analyser didn't run")
131
+ func testPeakAtFullScaleIsAccepted() {
132
+ // 1.000000 is the spec's encoding of digital full scale — a real
133
+ // measurement for a modern master, and the one most in need of the
134
+ // 1/peak clip cap.
135
+ assertPeak("1.000000", expected: 1.0)
136
+ assertPeak("1", expected: 1.0)
137
+ }
138
+
139
+ func testParsePeakRejectsEncoderZero() {
110
140
  XCTAssertNil(ReplayGainExtractor.parsePeak("0.0001"),
111
141
  "peak < 0.001 means encoder zero / no analysis")
112
142
  XCTAssertNil(ReplayGainExtractor.parsePeak("0"),
113
143
  "peak == 0 means encoder zero")
114
144
  }
115
145
 
146
+ func testParsePeakAcceptsRealMeasurementsEitherSideOfUnity() {
147
+ assertPeak("0.923366", expected: 0.923366)
148
+ assertPeak("1.160082", expected: 1.160082)
149
+ }
150
+
151
+ func testParsePeakAcceptsTheLowerBoundary() {
152
+ assertPeak("0.001", expected: 0.001, accuracy: 0.000001)
153
+ }
154
+
155
+ func testParsePeakRejectsJustBelowTheLowerBoundary() {
156
+ XCTAssertNil(ReplayGainExtractor.parsePeak("0.000999"))
157
+ }
158
+
159
+ func testParsePeakAcceptsTheUpperBoundary() {
160
+ assertPeak("2.0", expected: 2.0)
161
+ }
162
+
163
+ func testParsePeakRejectsJustAboveTheUpperBoundary() {
164
+ XCTAssertNil(ReplayGainExtractor.parsePeak("2.000001"))
165
+ }
166
+
167
+ func testParsePeakRejectsCommaDecimal() {
168
+ // RG2 is period-decimal, same as the gain path.
169
+ XCTAssertNil(ReplayGainExtractor.parsePeak("0,5"))
170
+ }
171
+
116
172
  func testParsePeakRejectsOutOfRange() {
117
173
  XCTAssertNil(ReplayGainExtractor.parsePeak("-0.5"))
118
174
  XCTAssertNil(ReplayGainExtractor.parsePeak("3.0"))
@@ -189,4 +245,158 @@ final class ReplayGainExtractorTests: XCTestCase {
189
245
  }
190
246
  }
191
247
 
248
+ // MARK: - acceptPeak — the numeric half of the same rule
249
+
250
+ func testAcceptPeakAcceptsFullScale() {
251
+ assertAcceptedPeak(1.0, expected: 1.0)
252
+ }
253
+
254
+ func testAcceptPeakAcceptsTheBoundaries() {
255
+ assertAcceptedPeak(0.001, expected: 0.001, accuracy: 0.000001)
256
+ assertAcceptedPeak(2.0, expected: 2.0)
257
+ }
258
+
259
+ func testAcceptPeakAcceptsRealMeasurementsEitherSideOfUnity() {
260
+ assertAcceptedPeak(0.923366, expected: 0.923366)
261
+ assertAcceptedPeak(1.160082, expected: 1.160082)
262
+ }
263
+
264
+ func testAcceptPeakRejectsValuesOutsideTheBoundaries() {
265
+ // Paired with an accepted value so the test still fails against an
266
+ // acceptor that rejects everything.
267
+ assertAcceptedPeak(0.5, expected: 0.5)
268
+ XCTAssertNil(ReplayGainExtractor.acceptPeak(0.000999))
269
+ XCTAssertNil(ReplayGainExtractor.acceptPeak(2.000001))
270
+ XCTAssertNil(ReplayGainExtractor.acceptPeak(0.0))
271
+ XCTAssertNil(ReplayGainExtractor.acceptPeak(-0.5))
272
+ XCTAssertNil(ReplayGainExtractor.acceptPeak(3.0))
273
+ }
274
+
275
+ func testAcceptPeakRejectsNonFinite() {
276
+ assertAcceptedPeak(1.0, expected: 1.0)
277
+ XCTAssertNil(ReplayGainExtractor.acceptPeak(.nan))
278
+ XCTAssertNil(ReplayGainExtractor.acceptPeak(.infinity))
279
+ XCTAssertNil(ReplayGainExtractor.acceptPeak(-.infinity))
280
+ }
281
+
282
+ // MARK: - acceptGainDb — the numeric half of the same rule
283
+
284
+ func testAcceptGainDbAcceptsAnyFiniteValue() {
285
+ assertAcceptedGain(-20.0, expected: -20.0)
286
+ assertAcceptedGain(-6.54, expected: -6.54)
287
+ assertAcceptedGain(0.0, expected: 0.0)
288
+ assertAcceptedGain(3.21, expected: 3.21)
289
+ }
290
+
291
+ func testAcceptGainDbAppliesNoRangeGuard() {
292
+ // Unlike a peak, a gain has no plausible-value window — the only
293
+ // rejection is non-finite.
294
+ assertAcceptedGain(100.0, expected: 100.0)
295
+ assertAcceptedGain(-100.0, expected: -100.0)
296
+ }
297
+
298
+ func testAcceptGainDbRejectsNonFinite() {
299
+ assertAcceptedGain(-6.0, expected: -6.0)
300
+ XCTAssertNil(ReplayGainExtractor.acceptGainDb(.nan))
301
+ XCTAssertNil(ReplayGainExtractor.acceptGainDb(.infinity))
302
+ XCTAssertNil(ReplayGainExtractor.acceptGainDb(-.infinity))
303
+ }
304
+
305
+ // MARK: - One rule, two entry points
306
+
307
+ func testPeakStringAndNumericPathsAgree() {
308
+ // The string parser is the numeric acceptor plus text handling, so
309
+ // every value that has both representations must land identically.
310
+ let cases: [(String, Double)] = [
311
+ ("1.000000", 1.0),
312
+ ("1", 1.0),
313
+ ("0.923366", 0.923366),
314
+ ("1.160082", 1.160082),
315
+ ("0.5", 0.5),
316
+ ("2.0", 2.0),
317
+ ("2.000001", 2.000001),
318
+ ("0.001", 0.001),
319
+ ("0.000999", 0.000999),
320
+ ("0", 0.0),
321
+ ("-0.5", -0.5),
322
+ ("3.0", 3.0),
323
+ ("nan", Double.nan),
324
+ ("inf", Double.infinity),
325
+ ("-inf", -Double.infinity),
326
+ ]
327
+ for (raw, value) in cases {
328
+ XCTAssertEqual(
329
+ ReplayGainExtractor.parsePeak(raw),
330
+ ReplayGainExtractor.acceptPeak(value),
331
+ "parsePeak(\"\(raw)\") and acceptPeak(\(value)) must agree"
332
+ )
333
+ }
334
+ }
335
+
336
+ func testGainDbStringAndNumericPathsAgree() {
337
+ let cases: [(String, Double)] = [
338
+ ("-20.00 dB", -20.0),
339
+ ("-6.54", -6.54),
340
+ ("+3.21 dB", 3.21),
341
+ ("0.00 dB", 0.0),
342
+ ("100.0", 100.0),
343
+ ("-100.0", -100.0),
344
+ ("nan dB", Double.nan),
345
+ ("inf", Double.infinity),
346
+ ("-inf", -Double.infinity),
347
+ ]
348
+ for (raw, value) in cases {
349
+ XCTAssertEqual(
350
+ ReplayGainExtractor.parseGainDb(raw),
351
+ ReplayGainExtractor.acceptGainDb(value),
352
+ "parseGainDb(\"\(raw)\") and acceptGainDb(\(value)) must agree"
353
+ )
354
+ }
355
+ }
356
+
357
+ // MARK: - Tag-set acceptance
358
+
359
+ // MARK: - Tag-set acceptance
360
+
361
+ func testRgDataFromTagsParsesAllFourFields() throws {
362
+ let data = try XCTUnwrap(ReplayGainExtractor.rgDataFromTags([
363
+ "REPLAYGAIN_TRACK_GAIN": "-12.0 dB",
364
+ "REPLAYGAIN_TRACK_PEAK": "0.95",
365
+ "REPLAYGAIN_ALBUM_GAIN": "-8.0 dB",
366
+ "REPLAYGAIN_ALBUM_PEAK": "0.90",
367
+ ]))
368
+ XCTAssertEqual(try XCTUnwrap(data.trackGainDb), -12.0, accuracy: 0.001)
369
+ XCTAssertEqual(try XCTUnwrap(data.trackPeak), 0.95, accuracy: 0.001)
370
+ XCTAssertEqual(try XCTUnwrap(data.albumGainDb), -8.0, accuracy: 0.001)
371
+ XCTAssertEqual(try XCTUnwrap(data.albumPeak), 0.90, accuracy: 0.001)
372
+ }
373
+
374
+ func testRgDataFromTagsAcceptsLowercaseKeys() throws {
375
+ let data = try XCTUnwrap(ReplayGainExtractor.rgDataFromTags([
376
+ "replaygain_track_gain": "-6.5 dB",
377
+ ]))
378
+ XCTAssertEqual(try XCTUnwrap(data.trackGainDb), -6.5, accuracy: 0.001)
379
+ }
380
+
381
+ func testRgDataFromTagsReturnsNilForEmptyDict() {
382
+ XCTAssertNil(ReplayGainExtractor.rgDataFromTags([:]))
383
+ }
384
+
385
+ func testRgDataFromTagsReturnsNilWhenAllValuesUnparseable() {
386
+ XCTAssertNil(ReplayGainExtractor.rgDataFromTags([
387
+ "REPLAYGAIN_TRACK_GAIN": "garbage",
388
+ "REPLAYGAIN_TRACK_PEAK": "3.0", // above the plausible range → rejected
389
+ ]))
390
+ }
391
+
392
+ func testRgDataFromTagsAcceptsAFullScalePeak() throws {
393
+ // 1.000000 is digital full scale — a measurement, so the tag set is
394
+ // usable even when it carries nothing else.
395
+ let data = try XCTUnwrap(ReplayGainExtractor.rgDataFromTags([
396
+ "REPLAYGAIN_TRACK_PEAK": "1.000000",
397
+ ]))
398
+ XCTAssertEqual(try XCTUnwrap(data.trackPeak), 1.0, accuracy: 0.0001)
399
+ }
400
+
401
+ // MARK: - Opus container guard
192
402
  }
@@ -0,0 +1,230 @@
1
+ import XCTest
2
+ @testable import QueuePlayer
3
+
4
+ /// Unit tests for `ReplayGainData.merge(suppliedClaimed:supplied:detected:)`
5
+ /// — the all-or-nothing, per-item choice between consumer-supplied
6
+ /// ReplayGain and values detected from the file.
7
+ ///
8
+ /// The rule is one predicate on the item, not two pair decisions and not
9
+ /// four field decisions: if the consumer supplied *any* of the four
10
+ /// fields, that track's ReplayGain comes solely from the consumer and
11
+ /// detection is discarded in full — including for the mode the consumer
12
+ /// said nothing about. Supplying a value is itself the statement of
13
+ /// intent; a consumer who wanted the file's own tags supplies nothing.
14
+ ///
15
+ /// `suppliedClaimed` is captured before validation, so
16
+ /// `suppliedClaimed == true` with `supplied == nil` is a real state
17
+ /// meaning "the consumer owns this track and gave nothing usable".
18
+ /// Ownership is triggered by field *presence*, not validity.
19
+ ///
20
+ /// Supplied and detected fixtures deliberately carry different values in
21
+ /// every field, so a row that returned the wrong source cannot pass.
22
+ final class ReplayGainMergeTests: XCTestCase {
23
+
24
+ // MARK: - Fixtures
25
+
26
+ private let detectedAll = ReplayGainData(
27
+ trackGainDb: -20.0, trackPeak: 0.5,
28
+ albumGainDb: -6.0, albumPeak: 0.6
29
+ )
30
+ private let detectedTrackOnly = ReplayGainData(
31
+ trackGainDb: -20.0, trackPeak: 0.5,
32
+ albumGainDb: nil, albumPeak: nil
33
+ )
34
+ private let detectedAlbumOnly = ReplayGainData(
35
+ trackGainDb: nil, trackPeak: nil,
36
+ albumGainDb: -6.0, albumPeak: 0.6
37
+ )
38
+
39
+ private let suppliedAll = ReplayGainData(
40
+ trackGainDb: -3.0, trackPeak: 0.25,
41
+ albumGainDb: -9.0, albumPeak: 0.3
42
+ )
43
+ private let suppliedTrackGain = ReplayGainData(
44
+ trackGainDb: -3.0, trackPeak: nil,
45
+ albumGainDb: nil, albumPeak: nil
46
+ )
47
+ private let suppliedAlbumGain = ReplayGainData(
48
+ trackGainDb: nil, trackPeak: nil,
49
+ albumGainDb: -9.0, albumPeak: nil
50
+ )
51
+ private let suppliedTrackPeak = ReplayGainData(
52
+ trackGainDb: nil, trackPeak: 0.25,
53
+ albumGainDb: nil, albumPeak: nil
54
+ )
55
+ private let suppliedAlbumPeak = ReplayGainData(
56
+ trackGainDb: nil, trackPeak: nil,
57
+ albumGainDb: nil, albumPeak: 0.3
58
+ )
59
+ private let suppliedTrackPair = ReplayGainData(
60
+ trackGainDb: -3.0, trackPeak: 0.25,
61
+ albumGainDb: nil, albumPeak: nil
62
+ )
63
+ private let suppliedAlbumPair = ReplayGainData(
64
+ trackGainDb: nil, trackPeak: nil,
65
+ albumGainDb: -9.0, albumPeak: 0.3
66
+ )
67
+
68
+ // MARK: - Helper
69
+
70
+ private func assertMerge(
71
+ claimed: Bool,
72
+ supplied: ReplayGainData?,
73
+ detected: ReplayGainData?,
74
+ equals expected: ReplayGainData?,
75
+ _ message: String,
76
+ file: StaticString = #file, line: UInt = #line
77
+ ) {
78
+ let actual = ReplayGainData.merge(
79
+ suppliedClaimed: claimed, supplied: supplied, detected: detected
80
+ )
81
+ XCTAssertEqual(actual, expected, message, file: file, line: line)
82
+ }
83
+
84
+ // MARK: - No claim — detection is used verbatim
85
+
86
+ func testAnUnclaimedTrackUsesEveryDetectedField() {
87
+ assertMerge(
88
+ claimed: false, supplied: nil, detected: detectedAll,
89
+ equals: detectedAll,
90
+ "no supplied field means detection is used, all four"
91
+ )
92
+ }
93
+
94
+ func testAnUnclaimedTrackWithNoDetectionHasNoReplayGain() {
95
+ assertMerge(
96
+ claimed: false, supplied: nil, detected: nil,
97
+ equals: nil,
98
+ "nothing supplied and nothing detected is no ReplayGain"
99
+ )
100
+ }
101
+
102
+ func testAnUnclaimedTrackKeepsATrackOnlyDetection() {
103
+ assertMerge(
104
+ claimed: false, supplied: nil, detected: detectedTrackOnly,
105
+ equals: detectedTrackOnly,
106
+ "a partial detection is used as-is, not padded"
107
+ )
108
+ }
109
+
110
+ func testAnUnclaimedTrackKeepsAnAlbumOnlyDetection() {
111
+ assertMerge(
112
+ claimed: false, supplied: nil, detected: detectedAlbumOnly,
113
+ equals: detectedAlbumOnly,
114
+ "the mirrored partial-detection case"
115
+ )
116
+ }
117
+
118
+ // MARK: - Claimed — one supplied field discards all detection
119
+
120
+ func testASuppliedTrackGainDiscardsEveryDetectedField() {
121
+ assertMerge(
122
+ claimed: true, supplied: suppliedTrackGain, detected: detectedAll,
123
+ equals: suppliedTrackGain,
124
+ "supplied trackGain applies; detected peak and album pair are dropped"
125
+ )
126
+ }
127
+
128
+ func testASuppliedAlbumGainDiscardsEveryDetectedField() {
129
+ assertMerge(
130
+ claimed: true, supplied: suppliedAlbumGain, detected: detectedAll,
131
+ equals: suppliedAlbumGain,
132
+ "supplied albumGain applies; detected track pair and album peak are dropped"
133
+ )
134
+ }
135
+
136
+ func testASuppliedTrackPeakAloneLeavesNoGainAnywhere() {
137
+ assertMerge(
138
+ claimed: true, supplied: suppliedTrackPeak, detected: detectedAll,
139
+ equals: suppliedTrackPeak,
140
+ "a peak with no gain is ownership with nothing to apply"
141
+ )
142
+ }
143
+
144
+ func testASuppliedAlbumPeakAloneLeavesNoGainAnywhere() {
145
+ assertMerge(
146
+ claimed: true, supplied: suppliedAlbumPeak, detected: detectedAll,
147
+ equals: suppliedAlbumPeak,
148
+ "the mirrored peak-only case"
149
+ )
150
+ }
151
+
152
+ func testASuppliedTrackPairDiscardsTheDetectedAlbumPair() {
153
+ assertMerge(
154
+ claimed: true, supplied: suppliedTrackPair, detected: detectedAll,
155
+ equals: suppliedTrackPair,
156
+ "album mode plays at unity even though the file carries album tags"
157
+ )
158
+ }
159
+
160
+ func testASuppliedAlbumPairDiscardsTheDetectedTrackPair() {
161
+ assertMerge(
162
+ claimed: true, supplied: suppliedAlbumPair, detected: detectedAll,
163
+ equals: suppliedAlbumPair,
164
+ "track mode plays at unity even though the file carries track tags"
165
+ )
166
+ }
167
+
168
+ func testAFullySuppliedSetReplacesAFullyDetectedSet() {
169
+ assertMerge(
170
+ claimed: true, supplied: suppliedAll, detected: detectedAll,
171
+ equals: suppliedAll,
172
+ "all four supplied values win over all four detected values"
173
+ )
174
+ }
175
+
176
+ func testASuppliedTrackGainDiscardsAnAlbumOnlyDetection() {
177
+ assertMerge(
178
+ claimed: true, supplied: suppliedTrackGain, detected: detectedAlbumOnly,
179
+ equals: suppliedTrackGain,
180
+ "detection is discarded even where supplied and detected do not overlap"
181
+ )
182
+ }
183
+
184
+ // MARK: - Claimed with no detection to discard
185
+
186
+ func testAFullySuppliedSetAppliesToAnUntaggedFile() {
187
+ assertMerge(
188
+ claimed: true, supplied: suppliedAll, detected: nil,
189
+ equals: suppliedAll,
190
+ "the motivating case — a server that stripped tags on transcode"
191
+ )
192
+ }
193
+
194
+ func testASuppliedTrackGainAppliesToAnUntaggedFile() {
195
+ assertMerge(
196
+ claimed: true, supplied: suppliedTrackGain, detected: nil,
197
+ equals: suppliedTrackGain,
198
+ "a single supplied field on an untagged file applies alone"
199
+ )
200
+ }
201
+
202
+ // MARK: - Claimed but nothing usable — ownership without a value
203
+
204
+ func testAClaimWithNoUsableValueDiscardsAFullDetection() {
205
+ // Every supplied value was rejected by validation, so `supplied` is
206
+ // nil — but the claim was captured before validation, so the file's
207
+ // tags do not come back.
208
+ assertMerge(
209
+ claimed: true, supplied: nil, detected: detectedAll,
210
+ equals: nil,
211
+ "presence, not validity, is what claims the track"
212
+ )
213
+ }
214
+
215
+ func testAClaimWithNoUsableValueDiscardsAPartialDetection() {
216
+ assertMerge(
217
+ claimed: true, supplied: nil, detected: detectedTrackOnly,
218
+ equals: nil,
219
+ "a partial detection is discarded by a claim just as a full one is"
220
+ )
221
+ }
222
+
223
+ func testAClaimWithNoUsableValueAndNoDetectionHasNoReplayGain() {
224
+ assertMerge(
225
+ claimed: true, supplied: nil, detected: nil,
226
+ equals: nil,
227
+ "ownership claimed, nothing supplied, nothing detected"
228
+ )
229
+ }
230
+ }