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
@@ -1,8 +1,10 @@
1
1
  package com.margelo.nitro.queueplayer
2
2
 
3
3
  import androidx.media3.common.C
4
+ import androidx.media3.common.audio.AudioProcessingPipeline
4
5
  import androidx.media3.common.audio.AudioProcessor
5
6
  import androidx.media3.common.util.UnstableApi
7
+ import com.google.common.collect.ImmutableList
6
8
  import org.junit.Assert.assertEquals
7
9
  import org.junit.Assert.assertFalse
8
10
  import org.junit.Assert.assertTrue
@@ -17,8 +19,9 @@ import java.nio.ByteOrder
17
19
  * Unit coverage for [ReplayGainAudioProcessor] — the per-sample gain
18
20
  * stage Media3 wires into the AudioSink chain. Exercises the
19
21
  * [AudioProcessor] contract: configure, isActive gating, in-place multiply
20
- * on both 16-bit and float PCM, the 16-bit full-scale clamp, and the
21
- * encoding gate.
22
+ * on both 16-bit and float PCM, the 16-bit full-scale clamp, the
23
+ * encoding gate, and the unity pass-through that keeps the processor in
24
+ * the [AudioProcessingPipeline] while it applies nothing.
22
25
  */
23
26
  @RunWith(RobolectricTestRunner::class)
24
27
  @Config(sdk = [34])
@@ -26,13 +29,21 @@ import java.nio.ByteOrder
26
29
  class ReplayGainAudioProcessorTest {
27
30
 
28
31
  @Test
29
- fun `isActive is false at unity gain regardless of float configuration`() {
32
+ fun `isActive is true at unity gain on a Float32 input`() {
30
33
  val proc = ReplayGainAudioProcessor()
31
34
  proc.configure(floatFormat(48000, 2))
32
- assertFalse(proc.isActive)
35
+ assertTrue(proc.isActive)
33
36
 
34
37
  proc.setLinearGain(1.0f)
35
- assertFalse(proc.isActive)
38
+ assertTrue(proc.isActive)
39
+ }
40
+
41
+ @Test
42
+ fun `isActive is true at unity gain on a 16-bit input`() {
43
+ val proc = ReplayGainAudioProcessor()
44
+ proc.configure(int16Format(48000, 2))
45
+ proc.setLinearGain(1.0f)
46
+ assertTrue(proc.isActive)
36
47
  }
37
48
 
38
49
  @Test
@@ -51,6 +62,13 @@ class ReplayGainAudioProcessorTest {
51
62
  assertTrue(proc.isActive)
52
63
  }
53
64
 
65
+ @Test
66
+ fun `isActive is false before configure`() {
67
+ val proc = ReplayGainAudioProcessor()
68
+ proc.setLinearGain(0.5f)
69
+ assertFalse(proc.isActive)
70
+ }
71
+
54
72
  @Test
55
73
  fun `isActive is false on an unsupported encoding`() {
56
74
  val proc = ReplayGainAudioProcessor()
@@ -59,6 +77,88 @@ class ReplayGainAudioProcessorTest {
59
77
  assertFalse(proc.isActive)
60
78
  }
61
79
 
80
+ @Test
81
+ fun `queueInput at unity gain passes float samples through unchanged`() {
82
+ val proc = ReplayGainAudioProcessor()
83
+ proc.configure(floatFormat(48000, 1))
84
+ proc.setLinearGain(1.0f)
85
+
86
+ val input = floatBuffer(floatArrayOf(1.0f, 0.5f, -0.25f, 0.0f, -1.0f))
87
+ proc.queueInput(input)
88
+
89
+ val output = readFloats(proc.output, 5)
90
+ assertEquals(1.0f, output[0], 1e-6f)
91
+ assertEquals(0.5f, output[1], 1e-6f)
92
+ assertEquals(-0.25f, output[2], 1e-6f)
93
+ assertEquals(0.0f, output[3], 1e-6f)
94
+ assertEquals(-1.0f, output[4], 1e-6f)
95
+ assertEquals(0, input.remaining())
96
+ }
97
+
98
+ @Test
99
+ fun `queueInput at unity gain passes 16-bit samples through unchanged`() {
100
+ val proc = ReplayGainAudioProcessor()
101
+ proc.configure(int16Format(48000, 1))
102
+ proc.setLinearGain(1.0f)
103
+
104
+ val input = shortBuffer(shortArrayOf(32767, -32768, 10000, 0, -3))
105
+ proc.queueInput(input)
106
+
107
+ val output = readShorts(proc.output, 5)
108
+ assertEquals(32767, output[0].toInt())
109
+ assertEquals(-32768, output[1].toInt())
110
+ assertEquals(10000, output[2].toInt())
111
+ assertEquals(0, output[3].toInt())
112
+ assertEquals(-3, output[4].toInt())
113
+ assertEquals(0, input.remaining())
114
+ }
115
+
116
+ @Test
117
+ fun `a pipeline configured at unity gain stays operational`() {
118
+ val proc = ReplayGainAudioProcessor()
119
+ val pipeline = newPipeline(proc, int16Format(48000, 1))
120
+
121
+ assertTrue(
122
+ "the gain stage must stay in the active set so a later gain can reach the audio",
123
+ pipeline.isOperational
124
+ )
125
+ }
126
+
127
+ @Test
128
+ fun `a gain pushed after the pipeline is configured reaches the audio`() {
129
+ val proc = ReplayGainAudioProcessor()
130
+ // configure() + flush() is where Media3 fixes the active-processor set,
131
+ // and it runs at unity for a queue loaded while ReplayGain is off.
132
+ val pipeline = newPipeline(proc, int16Format(48000, 1))
133
+
134
+ // The gain arrives afterwards, as it does when the mode is selected
135
+ // mid-playback. Nothing reconfigures or flushes the sink.
136
+ proc.setLinearGain(0.5f)
137
+
138
+ val input = shortBuffer(shortArrayOf(10000, -8000, 4))
139
+ pipeline.queueInput(input)
140
+
141
+ assertTrue(pipeline.isOperational)
142
+ val output = drainShorts(pipeline, 3)
143
+ assertEquals(5000, output[0].toInt())
144
+ assertEquals(-4000, output[1].toInt())
145
+ assertEquals(2, output[2].toInt())
146
+ }
147
+
148
+ @Test
149
+ fun `a gain pushed then returned to unity stops reaching the audio`() {
150
+ val proc = ReplayGainAudioProcessor()
151
+ val pipeline = newPipeline(proc, int16Format(48000, 1))
152
+
153
+ proc.setLinearGain(0.5f)
154
+ pipeline.queueInput(shortBuffer(shortArrayOf(10000)))
155
+ assertEquals(5000, drainShorts(pipeline, 1)[0].toInt())
156
+
157
+ proc.setLinearGain(1.0f)
158
+ pipeline.queueInput(shortBuffer(shortArrayOf(10000)))
159
+ assertEquals(10000, drainShorts(pipeline, 1)[0].toInt())
160
+ }
161
+
62
162
  @Test
63
163
  fun `queueInput multiplies every sample by the linear gain in place`() {
64
164
  val proc = ReplayGainAudioProcessor()
@@ -156,6 +256,29 @@ class ReplayGainAudioProcessorTest {
156
256
 
157
257
  // region Helpers
158
258
 
259
+ /** A single-stage pipeline taken through Media3's configure-then-flush setup. */
260
+ private fun newPipeline(
261
+ proc: ReplayGainAudioProcessor,
262
+ format: AudioProcessor.AudioFormat,
263
+ ): AudioProcessingPipeline {
264
+ val pipeline = AudioProcessingPipeline(ImmutableList.of<AudioProcessor>(proc))
265
+ pipeline.configure(format)
266
+ pipeline.flush(AudioProcessor.StreamMetadata.DEFAULT)
267
+ return pipeline
268
+ }
269
+
270
+ /**
271
+ * Read [count] shorts of pipeline output and consume them the way
272
+ * `DefaultAudioSink` does — an undrained output buffer blocks the next
273
+ * [AudioProcessingPipeline.queueInput] from reaching the processors.
274
+ */
275
+ private fun drainShorts(pipeline: AudioProcessingPipeline, count: Int): ShortArray {
276
+ val buffer = pipeline.output
277
+ val values = readShorts(buffer, count)
278
+ buffer.position(buffer.position() + count * Short.SIZE_BYTES)
279
+ return values
280
+ }
281
+
159
282
  private fun floatFormat(sampleRate: Int, channelCount: Int) =
160
283
  AudioProcessor.AudioFormat(sampleRate, channelCount, C.ENCODING_PCM_FLOAT)
161
284
 
@@ -103,7 +103,7 @@ class ReplayGainExtractorTest {
103
103
 
104
104
  // endregion
105
105
 
106
- // region parsePeak tolerance and encoder-default detection
106
+ // region parsePeak acceptance band
107
107
 
108
108
  @Test
109
109
  fun `parsePeak reads the standard linear format`() {
@@ -113,25 +113,40 @@ class ReplayGainExtractorTest {
113
113
  }
114
114
 
115
115
  @Test
116
- fun `parsePeak rejects encoder defaults of exactly 1 and below 0_001`() {
117
- assertNull(
118
- "peak == exactly 1.0 means the analyser didn't run",
119
- ReplayGainExtractor.parsePeak("1.000000"),
120
- )
121
- assertNull(
122
- "peak == exactly 1.0 means the analyser didn't run",
123
- ReplayGainExtractor.parsePeak("1"),
124
- )
116
+ fun `parsePeak accepts a peak of exactly 1_0`() {
117
+ // 1.0 is the RG2 encoding of digital full scale — a real and
118
+ // common measurement for a modern master, and the value most in
119
+ // need of the 1/peak clip cap.
120
+ assertPeak("1.000000", 1.0f)
121
+ assertPeak("1", 1.0f)
122
+ }
123
+
124
+ @Test
125
+ fun `parsePeak accepts real measurements either side of unity`() {
126
+ assertPeak("0.923366", 0.923366f)
127
+ assertPeak("1.160082", 1.160082f)
128
+ }
129
+
130
+ @Test
131
+ fun `parsePeak accepts the 0_001 and 2_0 boundaries and rejects just outside them`() {
132
+ assertPeak("0.001", 0.001f)
133
+ assertPeak("2.0", 2.0f)
125
134
  assertNull(
126
- "peak < 0.001 means encoder zero / no analysis",
127
- ReplayGainExtractor.parsePeak("0.0001"),
135
+ "0.000999 is below the encoder-zero floor",
136
+ ReplayGainExtractor.parsePeak("0.000999"),
128
137
  )
129
138
  assertNull(
130
- "peak == 0 means encoder zero",
131
- ReplayGainExtractor.parsePeak("0"),
139
+ "2.000001 is an implausible true peak",
140
+ ReplayGainExtractor.parsePeak("2.000001"),
132
141
  )
133
142
  }
134
143
 
144
+ @Test
145
+ fun `parsePeak rejects peaks below 0_001`() {
146
+ assertNull(ReplayGainExtractor.parsePeak("0.0001"))
147
+ assertNull(ReplayGainExtractor.parsePeak("0"))
148
+ }
149
+
135
150
  @Test
136
151
  fun `parsePeak rejects values outside the 0-to-2 range`() {
137
152
  assertNull(ReplayGainExtractor.parsePeak("-0.5"))
@@ -148,6 +163,138 @@ class ReplayGainExtractorTest {
148
163
 
149
164
  // endregion
150
165
 
166
+ // region acceptPeak — the numeric entry point
167
+
168
+ @Test
169
+ fun `acceptPeak accepts full scale and the real-measurement band`() {
170
+ assertAcceptedPeak(1.0, 1.0f)
171
+ assertAcceptedPeak(0.923366, 0.923366f)
172
+ assertAcceptedPeak(1.160082, 1.160082f)
173
+ assertAcceptedPeak(0.5, 0.5f)
174
+ }
175
+
176
+ @Test
177
+ fun `acceptPeak accepts the 0_001 and 2_0 boundaries and rejects just outside them`() {
178
+ assertAcceptedPeak(0.001, 0.001f)
179
+ assertAcceptedPeak(2.0, 2.0f)
180
+ assertNull("0.000999 is below the encoder-zero floor", ReplayGainExtractor.acceptPeak(0.000999))
181
+ assertNull("2.000001 is an implausible true peak", ReplayGainExtractor.acceptPeak(2.000001))
182
+ assertNull(ReplayGainExtractor.acceptPeak(0.0))
183
+ assertNull(ReplayGainExtractor.acceptPeak(-0.5))
184
+ assertNull(ReplayGainExtractor.acceptPeak(3.0))
185
+ }
186
+
187
+ @Test
188
+ fun `acceptPeak rejects a value above 2_0 that narrows to 2_0 as a Float`() {
189
+ // 2.0000001 is inside half a Float ulp of 2.0, so a range check
190
+ // applied after the Double -> Float narrowing sees 2.0f and lets
191
+ // it through. The 2.0 case is the positive control: this test
192
+ // must not be satisfiable by rejecting everything.
193
+ assertAcceptedPeak(2.0, 2.0f)
194
+ assertNull(ReplayGainExtractor.acceptPeak(2.0000001))
195
+ }
196
+
197
+ @Test
198
+ fun `acceptPeak rejects non-finite input`() {
199
+ assertAcceptedPeak(1.0, 1.0f)
200
+ assertNull(ReplayGainExtractor.acceptPeak(Double.NaN))
201
+ assertNull(ReplayGainExtractor.acceptPeak(Double.POSITIVE_INFINITY))
202
+ assertNull(ReplayGainExtractor.acceptPeak(Double.NEGATIVE_INFINITY))
203
+ }
204
+
205
+ // endregion
206
+
207
+ // region acceptGainDb — the numeric entry point
208
+
209
+ @Test
210
+ fun `acceptGainDb accepts every finite value with no plausibility band`() {
211
+ assertAcceptedGain(-20.0, -20.0f)
212
+ assertAcceptedGain(-6.54, -6.54f)
213
+ assertAcceptedGain(0.0, 0.0f)
214
+ assertAcceptedGain(6.0, 6.0f)
215
+ assertAcceptedGain(3.21, 3.21f)
216
+ // Physically absurd, still accepted — gain carries no range guard;
217
+ // clip safety is the clamp's job, not the acceptor's.
218
+ assertAcceptedGain(100.0, 100.0f)
219
+ assertAcceptedGain(-100.0, -100.0f)
220
+ }
221
+
222
+ @Test
223
+ fun `acceptGainDb rejects non-finite input`() {
224
+ assertAcceptedGain(-20.0, -20.0f)
225
+ assertNull(ReplayGainExtractor.acceptGainDb(Double.NaN))
226
+ assertNull(ReplayGainExtractor.acceptGainDb(Double.POSITIVE_INFINITY))
227
+ assertNull(ReplayGainExtractor.acceptGainDb(Double.NEGATIVE_INFINITY))
228
+ }
229
+
230
+ @Test
231
+ fun `acceptGainDb rejects a magnitude that narrows to Float infinity`() {
232
+ // Finite as a Double, infinite as a Float — the returned value has
233
+ // to be finite, so the check belongs after the narrowing.
234
+ assertAcceptedGain(-20.0, -20.0f)
235
+ assertNull(ReplayGainExtractor.acceptGainDb(1e40))
236
+ assertNull(ReplayGainExtractor.acceptGainDb(-1e40))
237
+ }
238
+
239
+ // endregion
240
+
241
+ // region String and numeric entry points share one rule
242
+
243
+ @Test
244
+ fun `parsePeak and acceptPeak agree on every value in the peak acceptance table`() {
245
+ val table = listOf(
246
+ "1.000000" to 1.0,
247
+ "1" to 1.0,
248
+ "0.923366" to 0.923366,
249
+ "1.160082" to 1.160082,
250
+ "0.5" to 0.5,
251
+ "2.0" to 2.0,
252
+ "2.000001" to 2.000001,
253
+ "0.001" to 0.001,
254
+ "0.000999" to 0.000999,
255
+ "0" to 0.0,
256
+ "-0.5" to -0.5,
257
+ "3.0" to 3.0,
258
+ "nan" to Double.NaN,
259
+ "inf" to Double.POSITIVE_INFINITY,
260
+ "-inf" to Double.NEGATIVE_INFINITY,
261
+ )
262
+ for ((raw, numeric) in table) {
263
+ assertSamePeakVerdict(raw, numeric)
264
+ }
265
+ }
266
+
267
+ @Test
268
+ fun `parseGainDb and acceptGainDb agree on every value in the gain acceptance table`() {
269
+ val table = listOf(
270
+ "-20.00 dB" to -20.0,
271
+ "-6.54" to -6.54,
272
+ "+3.21 dB" to 3.21,
273
+ "0.00 dB" to 0.0,
274
+ "100.0" to 100.0,
275
+ "-100.0" to -100.0,
276
+ "nan dB" to Double.NaN,
277
+ "inf" to Double.POSITIVE_INFINITY,
278
+ "-inf" to Double.NEGATIVE_INFINITY,
279
+ )
280
+ for ((raw, numeric) in table) {
281
+ assertSameGainVerdict(raw, numeric)
282
+ }
283
+ }
284
+
285
+ @Test
286
+ fun `comma decimals reject on the string path while the numeric path accepts the value`() {
287
+ // The period-decimal rule is a property of RG2 text, not of the
288
+ // number — so it lives in the string parser and the numeric
289
+ // acceptor takes the same value happily.
290
+ assertNull(ReplayGainExtractor.parsePeak("0,5"))
291
+ assertAcceptedPeak(0.5, 0.5f)
292
+ assertNull(ReplayGainExtractor.parseGainDb("-6,54 dB"))
293
+ assertAcceptedGain(-6.54, -6.54f)
294
+ }
295
+
296
+ // endregion
297
+
151
298
  // region canonicalKeyForName mapping
152
299
 
153
300
  @Test
@@ -450,6 +597,54 @@ class ReplayGainExtractorTest {
450
597
  assertEquals(expected, actual!!, accuracy)
451
598
  }
452
599
 
600
+ private fun assertAcceptedPeak(value: Double, expected: Float, accuracy: Float = 0.0001f) {
601
+ val actual = ReplayGainExtractor.acceptPeak(value)
602
+ assertNotNull("expected acceptPeak($value) to succeed", actual)
603
+ assertEquals(expected, actual!!, accuracy)
604
+ }
605
+
606
+ private fun assertAcceptedGain(value: Double, expected: Float, accuracy: Float = 0.001f) {
607
+ val actual = ReplayGainExtractor.acceptGainDb(value)
608
+ assertNotNull("expected acceptGainDb($value) to succeed", actual)
609
+ assertEquals(expected, actual!!, accuracy)
610
+ }
611
+
612
+ private fun assertSamePeakVerdict(raw: String, numeric: Double) {
613
+ val fromString = ReplayGainExtractor.parsePeak(raw)
614
+ val fromNumeric = ReplayGainExtractor.acceptPeak(numeric)
615
+ assertEquals(
616
+ "parsePeak('$raw') and acceptPeak($numeric) must agree on acceptance",
617
+ fromString == null,
618
+ fromNumeric == null,
619
+ )
620
+ if (fromString != null && fromNumeric != null) {
621
+ assertEquals(
622
+ "parsePeak('$raw') and acceptPeak($numeric) must agree on value",
623
+ fromString,
624
+ fromNumeric,
625
+ 0.0001f,
626
+ )
627
+ }
628
+ }
629
+
630
+ private fun assertSameGainVerdict(raw: String, numeric: Double) {
631
+ val fromString = ReplayGainExtractor.parseGainDb(raw)
632
+ val fromNumeric = ReplayGainExtractor.acceptGainDb(numeric)
633
+ assertEquals(
634
+ "parseGainDb('$raw') and acceptGainDb($numeric) must agree on acceptance",
635
+ fromString == null,
636
+ fromNumeric == null,
637
+ )
638
+ if (fromString != null && fromNumeric != null) {
639
+ assertEquals(
640
+ "parseGainDb('$raw') and acceptGainDb($numeric) must agree on value",
641
+ fromString,
642
+ fromNumeric,
643
+ 0.001f,
644
+ )
645
+ }
646
+ }
647
+
453
648
  private fun txxx(description: String, value: String): TextInformationFrame =
454
649
  TextInformationFrame(
455
650
  /* id = */ "TXXX",
@@ -94,30 +94,170 @@ class ReplayGainGainTest {
94
94
  }
95
95
 
96
96
  @Test
97
- fun `peak equal to 1 bypasses cap (encoder-default sentinel)`() {
98
- // The extractor already strips peak == 1.0; defending the math in
99
- // case any other code path produces such a ReplayGainData. With
100
- // +6 dB raw ≈ 1.995 and peak == 1.0, 1/peak == 1.0 would clamp
101
- // raw to 1.0 — but the contract is "peak == 1.0 means no useful
102
- // peak data", so the cap is bypassed.
97
+ fun `a full-scale peak caps the boost at unity`() {
98
+ // 1.0 is digital full scale, so 1/peak == 1.0 and a +6 dB boost
99
+ // (raw 1.995) has nowhere to go: any boost on a full-scale
100
+ // master clips. The cap binds to exactly unity.
103
101
  val data = ReplayGainData(
104
102
  trackGainDb = 6.0f,
105
103
  trackPeak = 1.0f,
106
104
  albumGainDb = null,
107
105
  albumPeak = null,
108
106
  )
109
- assertEquals(lin(6.0), computeLinearGain(data, ReplayGainMode.TRACK), 1e-6f)
107
+ assertEquals(1.0f, computeLinearGain(data, ReplayGainMode.TRACK), 1e-6f)
110
108
  }
111
109
 
112
110
  @Test
113
- fun `peak below 0_001 bypasses cap (encoder-zero sentinel)`() {
111
+ fun `a full-scale peak leaves attenuation alone`() {
112
+ // -20 dB raw = 0.100, well under the 1.0 cap — the cap is present
113
+ // and simply does not bind. Attenuation can never clip.
114
+ val data = ReplayGainData(
115
+ trackGainDb = -20.0f,
116
+ trackPeak = 1.0f,
117
+ albumGainDb = null,
118
+ albumPeak = null,
119
+ )
120
+ assertEquals(lin(-20.0), computeLinearGain(data, ReplayGainMode.TRACK), 1e-6f)
121
+ }
122
+
123
+ @Test
124
+ fun `an unusable peak clamps the boost to unity`() {
125
+ // A peak below 0.001 carries no usable measurement, and a peak
126
+ // cannot be derived from a gain — so boost is suppressed rather
127
+ // than run uncapped.
128
+ for (peak in listOf(0.0005f, 0.0001f)) {
129
+ val data = ReplayGainData(
130
+ trackGainDb = 6.0f,
131
+ trackPeak = peak,
132
+ albumGainDb = null,
133
+ albumPeak = null,
134
+ )
135
+ assertEquals(
136
+ "peak $peak carries no usable measurement",
137
+ 1.0f,
138
+ computeLinearGain(data, ReplayGainMode.TRACK),
139
+ 1e-6f,
140
+ )
141
+ }
142
+ }
143
+
144
+ @Test
145
+ fun `no peak at all clamps the boost to unity`() {
114
146
  val data = ReplayGainData(
115
147
  trackGainDb = 6.0f,
116
- trackPeak = 0.0001f,
148
+ trackPeak = null,
117
149
  albumGainDb = null,
118
150
  albumPeak = null,
119
151
  )
120
- assertEquals(lin(6.0), computeLinearGain(data, ReplayGainMode.TRACK), 1e-6f)
152
+ assertEquals(1.0f, computeLinearGain(data, ReplayGainMode.TRACK), 1e-6f)
153
+ }
154
+
155
+ @Test
156
+ fun `no peak at all leaves attenuation untouched`() {
157
+ // The positive half of the clamp: the boost suppression must not
158
+ // become a blanket clamp that also flattens cuts.
159
+ val data = ReplayGainData(
160
+ trackGainDb = -20.0f,
161
+ trackPeak = null,
162
+ albumGainDb = null,
163
+ albumPeak = null,
164
+ )
165
+ assertEquals(lin(-20.0), computeLinearGain(data, ReplayGainMode.TRACK), 1e-6f)
166
+ }
167
+
168
+ @Test
169
+ fun `a zero gain with no peak stays at unity`() {
170
+ val data = ReplayGainData(
171
+ trackGainDb = 0.0f,
172
+ trackPeak = null,
173
+ albumGainDb = null,
174
+ albumPeak = null,
175
+ )
176
+ assertEquals(1.0f, computeLinearGain(data, ReplayGainMode.TRACK), 1e-6f)
177
+ }
178
+
179
+ @Test
180
+ fun `a peak with headroom lets the full boost through`() {
181
+ // The positive control for the clamp rows above: peak 0.5 gives
182
+ // 1/peak = 2.0, so +6 dB (≈ 1.9953) fits underneath and is applied
183
+ // in full. Without this, "expect 1.000" everywhere is satisfied by
184
+ // an implementation that applies nothing at all.
185
+ val data = ReplayGainData(
186
+ trackGainDb = 6.0f,
187
+ trackPeak = 0.5f,
188
+ albumGainDb = null,
189
+ albumPeak = null,
190
+ )
191
+ assertEquals(1.9953f, computeLinearGain(data, ReplayGainMode.TRACK), 1e-4f)
192
+ }
193
+
194
+ @Test
195
+ fun `a boost beyond the headroom is capped at 1 over peak`() {
196
+ // +12 dB raw ≈ 3.981; peak 0.5 → 1/peak = 2.0 — cap binds.
197
+ val data = ReplayGainData(
198
+ trackGainDb = 12.0f,
199
+ trackPeak = 0.5f,
200
+ albumGainDb = null,
201
+ albumPeak = null,
202
+ )
203
+ assertEquals(2.0f, computeLinearGain(data, ReplayGainMode.TRACK), 1e-6f)
204
+ }
205
+
206
+ @Test
207
+ fun `a true peak above unity caps the boost below unity`() {
208
+ // Peaks above 1.0 are ordinary true-peak measurements: 1/1.16 =
209
+ // 0.862, so a +6 dB boost ends up as an attenuation.
210
+ val data = ReplayGainData(
211
+ trackGainDb = 6.0f,
212
+ trackPeak = 1.16f,
213
+ albumGainDb = null,
214
+ albumPeak = null,
215
+ )
216
+ assertEquals(1.0f / 1.16f, computeLinearGain(data, ReplayGainMode.TRACK), 1e-4f)
217
+ }
218
+
219
+ @Test
220
+ fun `the clip cap applies in album mode as well as track mode`() {
221
+ // Wiring the cap on the track pair alone passes every row above.
222
+ val noPeak = ReplayGainData(
223
+ trackGainDb = null,
224
+ trackPeak = null,
225
+ albumGainDb = 6.0f,
226
+ albumPeak = null,
227
+ )
228
+ assertEquals(
229
+ "album mode with no album peak clamps the boost to unity",
230
+ 1.0f,
231
+ computeLinearGain(noPeak, ReplayGainMode.ALBUM),
232
+ 1e-6f,
233
+ )
234
+ val fullScale = ReplayGainData(
235
+ trackGainDb = null,
236
+ trackPeak = null,
237
+ albumGainDb = 6.0f,
238
+ albumPeak = 1.0f,
239
+ )
240
+ assertEquals(
241
+ "album mode with a full-scale album peak caps the boost at unity",
242
+ 1.0f,
243
+ computeLinearGain(fullScale, ReplayGainMode.ALBUM),
244
+ 1e-6f,
245
+ )
246
+ }
247
+
248
+ @Test
249
+ fun `each mode caps against its own peak`() {
250
+ // Same gain in both modes, different peaks — the applied value has
251
+ // to differ, which shows the cap reads the selected pair's peak
252
+ // rather than whichever peak the data happens to carry.
253
+ val data = ReplayGainData(
254
+ trackGainDb = 6.0f,
255
+ trackPeak = 0.5f,
256
+ albumGainDb = 6.0f,
257
+ albumPeak = 1.16f,
258
+ )
259
+ assertEquals(1.9953f, computeLinearGain(data, ReplayGainMode.TRACK), 1e-4f)
260
+ assertEquals(1.0f / 1.16f, computeLinearGain(data, ReplayGainMode.ALBUM), 1e-4f)
121
261
  }
122
262
 
123
263
  @Test