react-native-queue-player 1.0.0 → 1.0.2
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.
- package/android/build.gradle +5 -0
- package/android/consumer-rules.pro +14 -0
- package/android/src/main/java/com/margelo/nitro/queueplayer/AudioPipelineRenderersFactory.kt +8 -10
- package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessor.kt +37 -25
- package/android/src/main/java/com/margelo/nitro/queueplayer/SignalsmithStretchAudioProcessor.kt +27 -19
- package/android/src/test/java/com/margelo/nitro/queueplayer/AudioPipelineRenderersFactoryTest.kt +14 -13
- package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessorTest.kt +64 -5
- package/package.json +6 -2
package/android/build.gradle
CHANGED
|
@@ -71,6 +71,11 @@ android {
|
|
|
71
71
|
targetSdkVersion getExtOrDefault("targetSdkVersion")
|
|
72
72
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
|
73
73
|
|
|
74
|
+
// Shipped in the AAR and applied to the consumer's R8 run so a consuming
|
|
75
|
+
// app's release build needs no manual rules for this library's transitive
|
|
76
|
+
// dependencies (e.g. Ktor's java.lang.management reference on Android).
|
|
77
|
+
consumerProguardFiles "consumer-rules.pro"
|
|
78
|
+
|
|
74
79
|
externalNativeBuild {
|
|
75
80
|
cmake {
|
|
76
81
|
cppFlags "-frtti -fexceptions -Wall -fstack-protector-all"
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# React Native Queue Player — consumer R8 / ProGuard rules.
|
|
2
|
+
#
|
|
3
|
+
# Packaged into the published AAR via `consumerProguardFiles` and applied
|
|
4
|
+
# automatically to a consuming app's R8 run, so consumers need no manual rules
|
|
5
|
+
# for this library's transitive dependencies.
|
|
6
|
+
|
|
7
|
+
# The embedded Ktor CIO HTTP server (the Android AirPlay 2 receiver) ships
|
|
8
|
+
# io.ktor.util.debug.IntellijIdeaDebugDetector, which references
|
|
9
|
+
# java.lang.management.ManagementFactory and RuntimeMXBean to detect an attached
|
|
10
|
+
# IntelliJ debugger. Those JMX classes are JVM-only and absent on Android, so a
|
|
11
|
+
# consumer's R8 release build (full mode treats missing classes as errors) fails
|
|
12
|
+
# on the missing references. The detector is debug tooling that never executes
|
|
13
|
+
# at runtime on Android; ignore the missing classes rather than fail the build.
|
|
14
|
+
-dontwarn java.lang.management.**
|
package/android/src/main/java/com/margelo/nitro/queueplayer/AudioPipelineRenderersFactory.kt
CHANGED
|
@@ -23,12 +23,14 @@ import androidx.media3.exoplayer.audio.DefaultAudioSink
|
|
|
23
23
|
* interleave their `flush` + buffer state. The CrossfadeEngine builds two
|
|
24
24
|
* factories (one per leg); each receives its own chain.
|
|
25
25
|
*
|
|
26
|
-
* Float
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
26
|
+
* Float output is deliberately NOT enabled. With Media3 float output, a hi-res
|
|
27
|
+
* (24-bit+) source produces a float sink, and [DefaultAudioSink.configure] then
|
|
28
|
+
* DROPS the entire custom [AudioProcessorChain] — its float branch omits the
|
|
29
|
+
* chain because the built-in Sonic outputs 16-bit — silently disabling playback
|
|
30
|
+
* speed, pitch-correction, ReplayGain, and the visualizer on hi-res content.
|
|
31
|
+
* Keeping the sink at 16-bit (Media3 downconverts hi-res) keeps the chain in the
|
|
32
|
+
* pipeline for every source; [ReplayGainAudioProcessor] and
|
|
33
|
+
* [SignalsmithStretchAudioProcessor] both process 16-bit PCM directly.
|
|
32
34
|
*/
|
|
33
35
|
@UnstableApi
|
|
34
36
|
internal class AudioPipelineRenderersFactory(
|
|
@@ -36,10 +38,6 @@ internal class AudioPipelineRenderersFactory(
|
|
|
36
38
|
private val audioProcessorChain: AudioProcessorChain,
|
|
37
39
|
) : DefaultRenderersFactory(context) {
|
|
38
40
|
|
|
39
|
-
init {
|
|
40
|
-
setEnableAudioFloatOutput(true)
|
|
41
|
-
}
|
|
42
|
-
|
|
43
41
|
override fun buildAudioSink(
|
|
44
42
|
context: Context,
|
|
45
43
|
enableFloatOutput: Boolean,
|
|
@@ -5,23 +5,22 @@ import androidx.media3.common.audio.AudioProcessor
|
|
|
5
5
|
import androidx.media3.common.audio.BaseAudioProcessor
|
|
6
6
|
import androidx.media3.common.util.UnstableApi
|
|
7
7
|
import java.nio.ByteBuffer
|
|
8
|
+
import kotlin.math.roundToInt
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Standalone Media3 [AudioProcessor] that applies a per-track linear
|
|
11
|
-
* gain to a
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* arrival.
|
|
12
|
+
* gain to a PCM stream. No knowledge of the engine, the EQ stage, or the
|
|
13
|
+
* visualizer pipeline — purely "given a PCM buffer, multiply by
|
|
14
|
+
* [linearGain]". The owning engine pushes a new gain via [setLinearGain]
|
|
15
|
+
* from its `Player.Listener` callbacks; `onMediaItemTransition` resets to
|
|
16
|
+
* unity until the next track's tags arrive.
|
|
17
17
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* and
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* non-Float32 no-op limitation in NOTES.md §25.
|
|
18
|
+
* Handles both 16-bit and float PCM. The sink runs at 16-bit (see
|
|
19
|
+
* [AudioPipelineRenderersFactory] — float output would drop the whole
|
|
20
|
+
* chain on hi-res sources), so a 16-bit block is scaled in float precision
|
|
21
|
+
* and narrowed back with a round + full-scale clamp; a float block is
|
|
22
|
+
* scaled in place. The gain is already clip-capped against the track peak
|
|
23
|
+
* (`ReplayGainGain`), so the clamp only guards sentinel/missing-peak tags.
|
|
25
24
|
*/
|
|
26
25
|
@UnstableApi
|
|
27
26
|
internal class ReplayGainAudioProcessor : BaseAudioProcessor() {
|
|
@@ -37,11 +36,11 @@ internal class ReplayGainAudioProcessor : BaseAudioProcessor() {
|
|
|
37
36
|
private var linearGain: Float = 1.0f
|
|
38
37
|
|
|
39
38
|
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
39
|
+
* Input PCM encoding captured at [onConfigure]. Volatile for the same
|
|
40
|
+
* render-thread / main-thread split as [linearGain].
|
|
42
41
|
*/
|
|
43
42
|
@Volatile
|
|
44
|
-
private var
|
|
43
|
+
private var encoding: Int = C.ENCODING_INVALID
|
|
45
44
|
|
|
46
45
|
/**
|
|
47
46
|
* Push a new linear gain. `1.0f` disables the processor's per-sample
|
|
@@ -52,7 +51,7 @@ internal class ReplayGainAudioProcessor : BaseAudioProcessor() {
|
|
|
52
51
|
}
|
|
53
52
|
|
|
54
53
|
override fun onConfigure(input: AudioProcessor.AudioFormat): AudioProcessor.AudioFormat {
|
|
55
|
-
|
|
54
|
+
encoding = input.encoding
|
|
56
55
|
// Pass-through: this stage doesn't change rate, channel count, or
|
|
57
56
|
// encoding. Returning [AudioProcessor.AudioFormat.NOT_SET] would
|
|
58
57
|
// tell Media3 the processor consumes the stream and drops the
|
|
@@ -61,23 +60,36 @@ internal class ReplayGainAudioProcessor : BaseAudioProcessor() {
|
|
|
61
60
|
}
|
|
62
61
|
|
|
63
62
|
override fun isActive(): Boolean =
|
|
64
|
-
super.isActive() && linearGain != 1.0f &&
|
|
63
|
+
super.isActive() && linearGain != 1.0f &&
|
|
64
|
+
(encoding == C.ENCODING_PCM_FLOAT || encoding == C.ENCODING_PCM_16BIT)
|
|
65
65
|
|
|
66
66
|
override fun queueInput(input: ByteBuffer) {
|
|
67
67
|
val gain = linearGain
|
|
68
68
|
val byteCount = input.remaining()
|
|
69
69
|
if (byteCount == 0) return
|
|
70
70
|
val out = replaceOutputBuffer(byteCount)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
71
|
+
if (encoding == C.ENCODING_PCM_FLOAT) {
|
|
72
|
+
val inF = input.asFloatBuffer()
|
|
73
|
+
val outF = out.asFloatBuffer()
|
|
74
|
+
while (inF.hasRemaining()) {
|
|
75
|
+
outF.put(inF.get() * gain)
|
|
76
|
+
}
|
|
77
|
+
} else {
|
|
78
|
+
// 16-bit: scale in float precision, narrow back with round + clamp.
|
|
79
|
+
// The gain is peak-capped upstream, so the clamp only guards
|
|
80
|
+
// sentinel/missing-peak tags against wraparound.
|
|
81
|
+
val inS = input.asShortBuffer()
|
|
82
|
+
val outS = out.asShortBuffer()
|
|
83
|
+
while (inS.hasRemaining()) {
|
|
84
|
+
val scaled = inS.get() * gain
|
|
85
|
+
outS.put(scaled.roundToInt().coerceIn(-32768, 32767).toShort())
|
|
86
|
+
}
|
|
75
87
|
}
|
|
76
88
|
input.position(input.limit())
|
|
77
89
|
// Advance the byte-level position so [getOutput] hands back a
|
|
78
|
-
// buffer with the produced bytes between position and limit
|
|
79
|
-
//
|
|
80
|
-
//
|
|
90
|
+
// buffer with the produced bytes between position and limit — the
|
|
91
|
+
// typed-view writes mutate the underlying bytes but don't move the
|
|
92
|
+
// parent ByteBuffer's position.
|
|
81
93
|
out.position(byteCount)
|
|
82
94
|
out.flip()
|
|
83
95
|
}
|
package/android/src/main/java/com/margelo/nitro/queueplayer/SignalsmithStretchAudioProcessor.kt
CHANGED
|
@@ -56,11 +56,12 @@ internal class SignalsmithStretchAudioProcessor : BaseAudioProcessor() {
|
|
|
56
56
|
private var floatIn: ByteBuffer? = null
|
|
57
57
|
private var floatOut: ByteBuffer? = null
|
|
58
58
|
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
-
|
|
59
|
+
// True once a stretched block has been produced, so the engine's tail
|
|
60
|
+
// latency awaits a drain. [onQueueEndOfStream] drains it exactly once and
|
|
61
|
+
// clears this — the native `outputLatency()` reports the fixed algorithmic
|
|
62
|
+
// latency (not the live buffer fill), so re-draining re-emits the same tail
|
|
63
|
+
// frames (an audible stutter) and never ends the stream.
|
|
64
|
+
private var pendingDrain = false
|
|
64
65
|
|
|
65
66
|
// Fractional frame remainder so per-block outFrames average to inFrames/speed.
|
|
66
67
|
private var frameCarry: Double = 0.0
|
|
@@ -73,13 +74,16 @@ internal class SignalsmithStretchAudioProcessor : BaseAudioProcessor() {
|
|
|
73
74
|
fun isStretching(): Boolean = isActive && speed != 1f
|
|
74
75
|
|
|
75
76
|
/**
|
|
76
|
-
* Map a playout duration back to media duration
|
|
77
|
-
* `
|
|
78
|
-
* the
|
|
77
|
+
* Map a playout duration back to media duration. For a pure time-stretch,
|
|
78
|
+
* `s` seconds of media play per playout second at speed `s`, so media =
|
|
79
|
+
* playout * speed — computed from the live speed rather than a cumulative
|
|
80
|
+
* input/output byte ratio, which mis-maps while the engine's latency is
|
|
81
|
+
* drained (the drained tail inflates output-vs-input) and freezes the
|
|
82
|
+
* position clock on a speed change.
|
|
79
83
|
*/
|
|
80
84
|
fun getMediaDuration(playoutDuration: Long): Long =
|
|
81
|
-
if (
|
|
82
|
-
else Util.scaleLargeTimestamp(playoutDuration,
|
|
85
|
+
if (speed == 1f) playoutDuration
|
|
86
|
+
else Util.scaleLargeTimestamp(playoutDuration, (speed * 1_000_000f).toLong(), 1_000_000L)
|
|
83
87
|
|
|
84
88
|
override fun onConfigure(inputAudioFormat: AudioProcessor.AudioFormat): AudioProcessor.AudioFormat {
|
|
85
89
|
encoding = inputAudioFormat.encoding
|
|
@@ -87,8 +91,11 @@ internal class SignalsmithStretchAudioProcessor : BaseAudioProcessor() {
|
|
|
87
91
|
sampleRateHz = inputAudioFormat.sampleRate
|
|
88
92
|
bytesPerFrame = channelCount * if (encoding == C.ENCODING_PCM_FLOAT) 4 else 2
|
|
89
93
|
// A reconfigure invalidates any engine built for the previous format;
|
|
90
|
-
// the next stretching queueInput lazily rebuilds it.
|
|
94
|
+
// the next stretching queueInput lazily rebuilds it. Clear the drain flag
|
|
95
|
+
// so the invariant holds locally, independent of the flush that Media3
|
|
96
|
+
// issues after a (re)configure.
|
|
91
97
|
releaseNative()
|
|
98
|
+
pendingDrain = false
|
|
92
99
|
// Pass-through format — the stretch changes duration, not rate/channels/
|
|
93
100
|
// encoding. Returning NOT_SET would drop the sink.
|
|
94
101
|
return inputAudioFormat
|
|
@@ -148,13 +155,17 @@ internal class SignalsmithStretchAudioProcessor : BaseAudioProcessor() {
|
|
|
148
155
|
}
|
|
149
156
|
|
|
150
157
|
input.position(input.limit())
|
|
151
|
-
|
|
152
|
-
outputBytes += outFrames.toLong() * bytesPerFrame
|
|
158
|
+
pendingDrain = true
|
|
153
159
|
}
|
|
154
160
|
|
|
155
161
|
override fun onQueueEndOfStream() {
|
|
156
162
|
val stretch = nativeStretch ?: return
|
|
157
|
-
|
|
163
|
+
// Drain the engine's tail latency exactly once per stretched span. Media3
|
|
164
|
+
// can call this repeatedly around a reconfigure; `outputLatency()` reports
|
|
165
|
+
// the fixed algorithmic latency, so re-draining re-emits the same tail
|
|
166
|
+
// frames (stutter) and the stream never ends, stalling a speed change.
|
|
167
|
+
if (!pendingDrain || speed == 1f) return
|
|
168
|
+
pendingDrain = false
|
|
158
169
|
val latency = stretch.outputLatency()
|
|
159
170
|
if (latency <= 0) return
|
|
160
171
|
if (encoding == C.ENCODING_PCM_FLOAT) {
|
|
@@ -176,14 +187,12 @@ internal class SignalsmithStretchAudioProcessor : BaseAudioProcessor() {
|
|
|
176
187
|
out.position(latency * bytesPerFrame)
|
|
177
188
|
out.flip()
|
|
178
189
|
}
|
|
179
|
-
outputBytes += latency.toLong() * bytesPerFrame
|
|
180
190
|
}
|
|
181
191
|
|
|
182
192
|
override fun onFlush(streamMetadata: AudioProcessor.StreamMetadata) {
|
|
183
193
|
nativeStretch?.reset()
|
|
184
194
|
frameCarry = 0.0
|
|
185
|
-
|
|
186
|
-
outputBytes = 0L
|
|
195
|
+
pendingDrain = false
|
|
187
196
|
}
|
|
188
197
|
|
|
189
198
|
override fun onReset() {
|
|
@@ -191,8 +200,7 @@ internal class SignalsmithStretchAudioProcessor : BaseAudioProcessor() {
|
|
|
191
200
|
floatIn = null
|
|
192
201
|
floatOut = null
|
|
193
202
|
frameCarry = 0.0
|
|
194
|
-
|
|
195
|
-
outputBytes = 0L
|
|
203
|
+
pendingDrain = false
|
|
196
204
|
}
|
|
197
205
|
|
|
198
206
|
private fun ensureNative(): SignalsmithStretchNative {
|
package/android/src/test/java/com/margelo/nitro/queueplayer/AudioPipelineRenderersFactoryTest.kt
CHANGED
|
@@ -3,8 +3,8 @@ package com.margelo.nitro.queueplayer
|
|
|
3
3
|
import androidx.media3.common.util.UnstableApi
|
|
4
4
|
import androidx.media3.exoplayer.DefaultRenderersFactory
|
|
5
5
|
import androidx.test.core.app.ApplicationProvider
|
|
6
|
+
import org.junit.Assert.assertFalse
|
|
6
7
|
import org.junit.Assert.assertNotNull
|
|
7
|
-
import org.junit.Assert.assertTrue
|
|
8
8
|
import org.junit.Test
|
|
9
9
|
import org.junit.runner.RunWith
|
|
10
10
|
import org.robolectric.RobolectricTestRunner
|
|
@@ -13,8 +13,8 @@ import java.lang.reflect.Field
|
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Unit coverage for [AudioPipelineRenderersFactory] construction — it wraps a
|
|
16
|
-
* caller-supplied [androidx.media3.common.audio.AudioProcessorChain] and
|
|
17
|
-
* the produced sink
|
|
16
|
+
* caller-supplied [androidx.media3.common.audio.AudioProcessorChain] and leaves
|
|
17
|
+
* the produced sink at 16-bit (float output off). The engines drive end-to-end exercise of
|
|
18
18
|
* `buildAudioSink` via `ExoPlayer.Builder.build()` in
|
|
19
19
|
* [GaplessEngineLifecycleTest] / [CrossfadeEngineLifecycleTest]; `buildAudioSink`
|
|
20
20
|
* itself is `protected` on [DefaultRenderersFactory] and cannot be called
|
|
@@ -36,21 +36,22 @@ class AudioPipelineRenderersFactoryTest {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
@Test
|
|
39
|
-
fun `factory
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
39
|
+
fun `factory leaves audio float output off so the chain survives on hi-res sources`() {
|
|
40
|
+
// Float output would make a hi-res (24-bit+) source produce a float sink,
|
|
41
|
+
// and DefaultAudioSink.configure()'s float branch DROPS the custom
|
|
42
|
+
// AudioProcessorChain — silently disabling playback speed, pitch-correction,
|
|
43
|
+
// ReplayGain, and the visualizer on hi-res content. Keeping the sink at
|
|
44
|
+
// 16-bit (Media3 downconverts hi-res) keeps the chain in the pipeline for
|
|
45
|
+
// every source. Pin that the factory leaves float output off (the
|
|
46
|
+
// DefaultRenderersFactory default) via reflection — no public accessor
|
|
47
|
+
// exists, but the field name is stable across Media3 1.x.
|
|
47
48
|
val factory =
|
|
48
49
|
AudioPipelineRenderersFactory(context, PitchAwareAudioProcessorChain(emptyArray()))
|
|
49
50
|
val field: Field =
|
|
50
51
|
DefaultRenderersFactory::class.java.getDeclaredField("enableFloatOutput")
|
|
51
52
|
field.isAccessible = true
|
|
52
|
-
|
|
53
|
-
"AudioPipelineRenderersFactory must enable float output",
|
|
53
|
+
assertFalse(
|
|
54
|
+
"AudioPipelineRenderersFactory must NOT enable float output (it drops the chain on hi-res)",
|
|
54
55
|
field.getBoolean(factory),
|
|
55
56
|
)
|
|
56
57
|
}
|
|
@@ -14,10 +14,11 @@ import java.nio.ByteBuffer
|
|
|
14
14
|
import java.nio.ByteOrder
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
|
-
* Unit coverage for [ReplayGainAudioProcessor] — the per-sample
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
17
|
+
* Unit coverage for [ReplayGainAudioProcessor] — the per-sample gain
|
|
18
|
+
* stage Media3 wires into the AudioSink chain. Exercises the
|
|
19
|
+
* [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.
|
|
21
22
|
*/
|
|
22
23
|
@RunWith(RobolectricTestRunner::class)
|
|
23
24
|
@Config(sdk = [34])
|
|
@@ -43,10 +44,18 @@ class ReplayGainAudioProcessorTest {
|
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
@Test
|
|
46
|
-
fun `isActive is
|
|
47
|
+
fun `isActive is true with non-unity gain on a 16-bit input`() {
|
|
47
48
|
val proc = ReplayGainAudioProcessor()
|
|
48
49
|
proc.configure(int16Format(48000, 2))
|
|
49
50
|
proc.setLinearGain(0.5f)
|
|
51
|
+
assertTrue(proc.isActive)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@Test
|
|
55
|
+
fun `isActive is false on an unsupported encoding`() {
|
|
56
|
+
val proc = ReplayGainAudioProcessor()
|
|
57
|
+
proc.configure(AudioProcessor.AudioFormat(48000, 2, C.ENCODING_PCM_24BIT))
|
|
58
|
+
proc.setLinearGain(0.5f)
|
|
50
59
|
assertFalse(proc.isActive)
|
|
51
60
|
}
|
|
52
61
|
|
|
@@ -82,6 +91,39 @@ class ReplayGainAudioProcessorTest {
|
|
|
82
91
|
assertEquals(0.6f, output[2], 1e-6f)
|
|
83
92
|
}
|
|
84
93
|
|
|
94
|
+
@Test
|
|
95
|
+
fun `queueInput scales 16-bit samples by the linear gain`() {
|
|
96
|
+
val proc = ReplayGainAudioProcessor()
|
|
97
|
+
proc.configure(int16Format(48000, 1))
|
|
98
|
+
proc.setLinearGain(0.5f)
|
|
99
|
+
|
|
100
|
+
val input = shortBuffer(shortArrayOf(10000, -8000, 4, 0, -10000))
|
|
101
|
+
proc.queueInput(input)
|
|
102
|
+
|
|
103
|
+
val output = readShorts(proc.output, 5)
|
|
104
|
+
assertEquals(5000, output[0].toInt())
|
|
105
|
+
assertEquals(-4000, output[1].toInt())
|
|
106
|
+
assertEquals(2, output[2].toInt())
|
|
107
|
+
assertEquals(0, output[3].toInt())
|
|
108
|
+
assertEquals(-5000, output[4].toInt())
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
@Test
|
|
112
|
+
fun `queueInput clamps a 16-bit boost that exceeds full scale`() {
|
|
113
|
+
val proc = ReplayGainAudioProcessor()
|
|
114
|
+
proc.configure(int16Format(44100, 1))
|
|
115
|
+
proc.setLinearGain(2.0f)
|
|
116
|
+
|
|
117
|
+
// 20000*2 = 40000 > 32767 and -20000*2 = -40000 < -32768 both clamp.
|
|
118
|
+
val input = shortBuffer(shortArrayOf(20000, -20000, 1000))
|
|
119
|
+
proc.queueInput(input)
|
|
120
|
+
|
|
121
|
+
val output = readShorts(proc.output, 3)
|
|
122
|
+
assertEquals(32767, output[0].toInt())
|
|
123
|
+
assertEquals(-32768, output[1].toInt())
|
|
124
|
+
assertEquals(2000, output[2].toInt())
|
|
125
|
+
}
|
|
126
|
+
|
|
85
127
|
@Test
|
|
86
128
|
fun `queueInput consumes the input buffer`() {
|
|
87
129
|
val proc = ReplayGainAudioProcessor()
|
|
@@ -137,5 +179,22 @@ class ReplayGainAudioProcessorTest {
|
|
|
137
179
|
return out
|
|
138
180
|
}
|
|
139
181
|
|
|
182
|
+
private fun shortBuffer(values: ShortArray): ByteBuffer {
|
|
183
|
+
val buffer = ByteBuffer
|
|
184
|
+
.allocateDirect(values.size * Short.SIZE_BYTES)
|
|
185
|
+
.order(ByteOrder.nativeOrder())
|
|
186
|
+
val asShort = buffer.asShortBuffer()
|
|
187
|
+
for (v in values) asShort.put(v)
|
|
188
|
+
buffer.position(0).limit(values.size * Short.SIZE_BYTES)
|
|
189
|
+
return buffer
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private fun readShorts(buffer: ByteBuffer, count: Int): ShortArray {
|
|
193
|
+
val out = ShortArray(count)
|
|
194
|
+
val asShort = buffer.asShortBuffer()
|
|
195
|
+
for (i in 0 until count) out[i] = asShort.get()
|
|
196
|
+
return out
|
|
197
|
+
}
|
|
198
|
+
|
|
140
199
|
// endregion
|
|
141
200
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-queue-player",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Nitro Modules audio playback library for React Native — gapless, EQ, crossfade, automotive, voice, casting",
|
|
5
5
|
"main": "./lib/module/index.js",
|
|
6
6
|
"types": "./lib/typescript/index.d.ts",
|
|
@@ -39,7 +39,11 @@
|
|
|
39
39
|
"nitrogen": "nitrogen && bash scripts/strip-nitrogen-unused-let.sh",
|
|
40
40
|
"typecheck": "tsc",
|
|
41
41
|
"test": "jest",
|
|
42
|
-
"test:coverage": "jest --coverage"
|
|
42
|
+
"test:coverage": "jest --coverage",
|
|
43
|
+
"release:patch": "node scripts/release.mjs patch",
|
|
44
|
+
"release:minor": "node scripts/release.mjs minor",
|
|
45
|
+
"release:major": "node scripts/release.mjs major",
|
|
46
|
+
"publish:npm": "npm publish --access public"
|
|
43
47
|
},
|
|
44
48
|
"keywords": [
|
|
45
49
|
"react-native",
|