mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-15 00:42:54 +01:00
playback: reject non-finite replay gain
Review follow-ups for the apply slice. The parse guard in the base branch keeps non-finite gains and peaks out of the library, but nothing here should depend on that: a value already stored, or written by a future caller, must not be able to silence a track or blast it at full scale. - the gain maths ignores a non-finite peak and falls back to unity when the result is not finite. - the processor falls back to unity for a non-finite gain, so `roundToInt` can never be handed a NaN. - the preamp is sanitised on both read and write, since `coerceIn` passes NaN straight through. - the renderers factory is pulled out of `PlaybackService` into `ReplayGainRenderersFactory` so a test can assert the sink really is built with the processor in its chain; the wiring was otherwise untested. - a null active source clears the gain rather than leaving the previous track's applied.
This commit is contained in:
@@ -213,6 +213,9 @@ class UserPreferencesTest {
|
||||
prefs.setReplayGainPreampDb(-99f)
|
||||
assertEquals(MIN_REPLAY_GAIN_PREAMP_DB, prefs.replayGainPreampDb().first(), 0f)
|
||||
|
||||
prefs.setReplayGainPreampDb(Float.NaN)
|
||||
assertEquals(0f, prefs.replayGainPreampDb().first(), 0f)
|
||||
|
||||
file.delete()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,24 @@ class ReplayGainProcessorTest {
|
||||
assertEquals(listOf<Byte>(1, 2, 3), readBytes(processor.getOutput()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun passesSamplesThroughWhenTheGainIsNaN() {
|
||||
val processor = processor(gain = Float.NaN)
|
||||
|
||||
processor.queueInput(sixteenBit(1234, -4321))
|
||||
|
||||
assertEquals(listOf(1234, -4321), readShorts(processor.getOutput()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun passesSamplesThroughWhenTheGainIsInfinite() {
|
||||
val processor = processor(gain = Float.POSITIVE_INFINITY)
|
||||
|
||||
processor.queueInput(sixteenBit(1234, -4321))
|
||||
|
||||
assertEquals(listOf(1234, -4321), readShorts(processor.getOutput()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun keepsTheInputFormatSoTheSinkKeepsItInThePipeline() {
|
||||
val processor = ReplayGainProcessor()
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.subtracks.playback
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.media3.common.audio.AudioProcessorChain
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.audio.DefaultAudioSink
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ReplayGainRenderersFactoryTest {
|
||||
@Test
|
||||
fun installsTheGainProcessorInTheAudioSink() {
|
||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
val processor = ReplayGainProcessor()
|
||||
|
||||
val sink = ReplayGainRenderersFactory(context, processor).buildAudioSink(context, false, false)
|
||||
|
||||
// DefaultAudioSink exposes no accessor for the chain it was built with, so this is the only
|
||||
// way to assert the processor is actually installed rather than merely constructed.
|
||||
val chain =
|
||||
DefaultAudioSink::class.java
|
||||
.getDeclaredField("audioProcessorChain")
|
||||
.apply { isAccessible = true }
|
||||
.get(sink) as AudioProcessorChain
|
||||
|
||||
assertEquals(true, chain.audioProcessors.any { it === processor })
|
||||
}
|
||||
}
|
||||
@@ -143,6 +143,26 @@ class ReplayGainTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun aNonFinitePeakDoesNotCap() {
|
||||
assertEquals(
|
||||
3.9810717f,
|
||||
replayGainLinearGain(
|
||||
ReplayGain(trackGain = 12f, trackPeak = Float.POSITIVE_INFINITY),
|
||||
ReplayGainSettings(ReplayGainMode.Track, 0f),
|
||||
),
|
||||
TOLERANCE,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun playsAtUnityWhenTheGainIsNotFinite() {
|
||||
val settings = ReplayGainSettings(ReplayGainMode.Track, 0f)
|
||||
|
||||
assertEquals(1f, replayGainLinearGain(ReplayGain(trackGain = Float.POSITIVE_INFINITY), settings), TOLERANCE)
|
||||
assertEquals(1f, replayGainLinearGain(ReplayGain(trackGain = Float.NaN), settings), TOLERANCE)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val TOLERANCE = 0.001f
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user