mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-10-20 06:28:27 +02:00
perf(usb): reuse PCM buffers and respect native ring capacity
This commit is contained in:
1 parent
644d035661
commit
4e20102bdd
7 files changed
+90
-18
No files matched your search
@@ -8,14 +8,17 @@ internal object BitPerfectPcm {
|
||||
fun outputBits(sourceBits: Int, supported: List<Int>): Int? =
|
||||
supported.filter { it in listOf(16, 24, 32) && it >= sourceBits }.minOrNull()
|
||||
|
||||
fun convert(input: ByteBuffer, inputBits: Int, sourceBits: Int, outputBits: Int, floating: Boolean): ByteBuffer {
|
||||
fun convert(input: ByteBuffer, inputBits: Int, sourceBits: Int, outputBits: Int, floating: Boolean, reuse: ByteBuffer? = null): ByteBuffer {
|
||||
require(sourceBits in listOf(16, 24, 32) && outputBits in listOf(16, 24, 32) && outputBits >= sourceBits)
|
||||
require(if (floating) inputBits == 32 && sourceBits <= 24 else inputBits >= sourceBits)
|
||||
val bytes = inputBits / 8
|
||||
require(input.remaining() % bytes == 0)
|
||||
input.order(ByteOrder.LITTLE_ENDIAN)
|
||||
val output = ByteBuffer.allocateDirect(input.remaining() / bytes * (outputBits / 8))
|
||||
.order(ByteOrder.LITTLE_ENDIAN)
|
||||
val size = input.remaining() / bytes * (outputBits / 8)
|
||||
val output = (reuse?.takeIf { it.isDirect && !it.isReadOnly && it.capacity() >= size }
|
||||
?: ByteBuffer.allocateDirect(size)).order(ByteOrder.LITTLE_ENDIAN)
|
||||
output.clear()
|
||||
output.limit(size)
|
||||
while (input.hasRemaining()) {
|
||||
val sample = if (floating) {
|
||||
val scaled = input.float.toDouble() * (1L shl (sourceBits - 1))
|
||||
|
||||
@@ -43,11 +43,6 @@ class UsbBitPerfectPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, Even
|
||||
methods.setMethodCallHandler(this)
|
||||
events.setStreamHandler(this)
|
||||
appContext = binding.applicationContext
|
||||
if (Build.VERSION.SDK_INT >= 34) {
|
||||
engine = UsbPcmPlayback(binding.applicationContext) { event ->
|
||||
main.post { sink?.success(event) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
||||
@@ -65,6 +60,9 @@ class UsbBitPerfectPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, Even
|
||||
main.post { sink?.success(event) }
|
||||
}
|
||||
}
|
||||
if (!useHiRes && !useDirect && Build.VERSION.SDK_INT >= 34 && engine == null) {
|
||||
engine = UsbPcmPlayback(appContext) { event -> main.post { sink?.success(event) } }
|
||||
}
|
||||
}
|
||||
if (useHiRes) {
|
||||
hiRes?.command(call) { value, error ->
|
||||
@@ -147,6 +145,7 @@ private class UsbPcmPlayback(context: Context, private val emit: (Map<String, An
|
||||
private var invalidRoute = false
|
||||
private var nextRouteCheck = 0L
|
||||
private var pending: ByteBuffer? = null
|
||||
private var converted: ByteBuffer? = null
|
||||
private val info = MediaCodec.BufferInfo()
|
||||
private val rawBuffer = ByteBuffer.allocateDirect(256 * 1024)
|
||||
@Volatile private var revision = 0
|
||||
@@ -308,7 +307,8 @@ private class UsbPcmPlayback(context: Context, private val emit: (Map<String, An
|
||||
positionBaseUs = timeUs + skipped * 1000000 / rate
|
||||
needTimestamp = false
|
||||
}
|
||||
pending = BitPerfectPcm.convert(input, inputBits, sourceBits, outputBits, floatInput)
|
||||
pending = BitPerfectPcm.convert(input, inputBits, sourceBits, outputBits, floatInput, converted)
|
||||
.also { converted = it }
|
||||
}
|
||||
|
||||
private fun decode() {
|
||||
@@ -482,6 +482,7 @@ private class UsbPcmPlayback(context: Context, private val emit: (Map<String, An
|
||||
revision++
|
||||
worker.post {
|
||||
closeSource()
|
||||
converted = null
|
||||
manager.removeOnPreferredMixerAttributesChangedListener(mixerListener)
|
||||
thread.quitSafely()
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ internal class UsbDirectPlayback(private val context: Context, private val emit:
|
||||
private var pcm: UsbPcmSource? = null
|
||||
private var dsd: DsdSource? = null
|
||||
private var pending: ByteBuffer? = null
|
||||
private var writeBuffer = ByteArray(0)
|
||||
private var playing = false
|
||||
private var ended = false
|
||||
private var failed = false
|
||||
@@ -222,9 +223,14 @@ internal class UsbDirectPlayback(private val context: Context, private val emit:
|
||||
val buffer = pending ?: return
|
||||
val count = minOf(buffer.remaining(), 65536) / frameBytes * frameBytes
|
||||
if (count == 0) { pending = null; return@repeat }
|
||||
val data = ByteArray(count)
|
||||
buffer.duplicate().get(data)
|
||||
val written = requireNotNull(output).write(data).toInt()
|
||||
val audio = requireNotNull(output)
|
||||
// There is one producer; the native consumer can only free more room
|
||||
// between this check and write. Wait rather than repeatedly copying
|
||||
// the same pending samples while the ring is full.
|
||||
if (audio.availableBytes().toInt() < count) return
|
||||
if (writeBuffer.size != count) writeBuffer = ByteArray(count)
|
||||
buffer.duplicate().get(writeBuffer)
|
||||
val written = audio.write(writeBuffer).toInt()
|
||||
buffer.position(buffer.position() + written)
|
||||
writtenFrames += written / frameBytes
|
||||
if (buffer.hasRemaining()) return
|
||||
@@ -321,6 +327,6 @@ internal class UsbDirectPlayback(private val context: Context, private val emit:
|
||||
permission?.done?.invoke(mapOf("reason" to "cancelled"), null)
|
||||
permission = null
|
||||
context.unregisterReceiver(receiver)
|
||||
worker.post { closeSource(); thread.quitSafely() }
|
||||
worker.post { closeSource(); writeBuffer = ByteArray(0); thread.quitSafely() }
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ internal class UsbPcmSource(path: String) : Closeable {
|
||||
private var codec: MediaCodec? = null
|
||||
private val info = MediaCodec.BufferInfo()
|
||||
private val raw = ByteBuffer.allocateDirect(256 * 1024)
|
||||
private var converted: ByteBuffer? = null
|
||||
private var inputEnded = false
|
||||
var ended = false
|
||||
private set
|
||||
@@ -91,7 +92,9 @@ internal class UsbPcmSource(path: String) : Closeable {
|
||||
require(frame > 0 && buffer.remaining() % frame == 0)
|
||||
val skip = if (timeUs < targetUs) ((targetUs - timeUs) * rate + 999999) / 1000000 else 0
|
||||
buffer.position(buffer.position() + minOf(skip, (buffer.remaining() / frame).toLong()).toInt() * frame)
|
||||
return BitPerfectPcm.convert(buffer, inputBits, bits, outputBits, floating)
|
||||
// The playback worker consumes this result before requesting another buffer.
|
||||
return BitPerfectPcm.convert(buffer, inputBits, bits, outputBits, floating, converted)
|
||||
.also { converted = it }
|
||||
}
|
||||
|
||||
fun read(outputBits: Int): ByteBuffer? {
|
||||
@@ -147,6 +150,7 @@ internal class UsbPcmSource(path: String) : Closeable {
|
||||
override fun close() {
|
||||
runCatching { codec?.release() }
|
||||
codec = null
|
||||
converted = null
|
||||
runCatching { extractor.release() }
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,8 @@ import kotlin.random.Random
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertSame
|
||||
import org.junit.Assert.assertNotSame
|
||||
import org.junit.Test
|
||||
|
||||
class BitPerfectPcmTest {
|
||||
@@ -37,6 +39,33 @@ class BitPerfectPcmTest {
|
||||
assertEquals(-65536, result.int)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun reusesWritableDirectCapacityAndResetsBoundsForShorterChunks() {
|
||||
val reusable = ByteBuffer.allocateDirect(32)
|
||||
reusable.position(10)
|
||||
reusable.limit(12)
|
||||
val input = ByteBuffer.wrap(byteArrayOf(1, 0, -1, -1))
|
||||
val result = BitPerfectPcm.convert(input, 16, 16, 24, false, reusable)
|
||||
assertSame(reusable, result)
|
||||
assertArrayEquals(byteArrayOf(0, 1, 0, 0, -1, -1), bytes(result))
|
||||
val shorter = BitPerfectPcm.convert(ByteBuffer.wrap(byteArrayOf(2, 0)), 16, 16, 16, false, result)
|
||||
assertSame(result, shorter)
|
||||
assertArrayEquals(byteArrayOf(2, 0), bytes(shorter))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun growsCapacityAndRejectsReadOnlyOrHeapBuffersForReuse() {
|
||||
val small = ByteBuffer.allocateDirect(2)
|
||||
val grown = BitPerfectPcm.convert(ByteBuffer.wrap(byteArrayOf(1, 0)), 16, 16, 32, false, small)
|
||||
assertNotSame(small, grown)
|
||||
assertArrayEquals(byteArrayOf(0, 0, 1, 0), bytes(grown))
|
||||
for (invalid in listOf(ByteBuffer.allocate(8), ByteBuffer.allocateDirect(8).asReadOnlyBuffer())) {
|
||||
val result = BitPerfectPcm.convert(ByteBuffer.wrap(byteArrayOf(3, 0)), 16, 16, 16, false, invalid)
|
||||
assertNotSame(invalid, result)
|
||||
assertArrayEquals(byteArrayOf(3, 0), bytes(result))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun decoderFloatRoundTripPreservesEveryBitOf24BitSamples() {
|
||||
val random = Random(17)
|
||||
|
||||
@@ -104,6 +104,14 @@ impl UsbDirectOutput {
|
||||
}
|
||||
}
|
||||
|
||||
/// Advisory, frame-aligned capacity before copying a buffer across FFI.
|
||||
pub fn available_bytes(&self) -> Result<u32, UsbAudioError> {
|
||||
#[cfg(target_os = "android")]
|
||||
return self.output.available_bytes().map_err(Into::into);
|
||||
#[cfg(not(target_os = "android"))]
|
||||
unreachable!("Android-only constructor")
|
||||
}
|
||||
|
||||
pub fn start(&self) -> Result<(), UsbAudioError> {
|
||||
#[cfg(target_os = "android")]
|
||||
return self.output.start().map_err(Into::into);
|
||||
|
||||
@@ -121,6 +121,18 @@ impl Output {
|
||||
self.shared.1.notify_all();
|
||||
Ok(n as u32)
|
||||
}
|
||||
pub fn available_bytes(&self) -> Result<u32, String> {
|
||||
let frame = self.format.subslot as usize * self.format.channels as usize;
|
||||
let s = self.shared.0.lock().unwrap();
|
||||
if let Some(e) = &s.error {
|
||||
return Err(e.clone());
|
||||
}
|
||||
if s.closed {
|
||||
return Err("USB output closed".into());
|
||||
}
|
||||
let capacity = (self.format.sample_rate as usize * frame / 5).max(256 * 1024);
|
||||
Ok((capacity.saturating_sub(s.bytes.len()) / frame * frame) as u32)
|
||||
}
|
||||
pub fn start(&self) -> Result<(), String> {
|
||||
let mut s = self.shared.0.lock().unwrap();
|
||||
if let Some(e) = &s.error {
|
||||
@@ -609,15 +621,24 @@ impl Session {
|
||||
s.frames += slot.music_frames;
|
||||
slot.music_frames = 0;
|
||||
}
|
||||
let lengths: Vec<_> = (0..packets).map(|_| clock.next() * frame).collect();
|
||||
let mut packet_lengths = [0usize; 32];
|
||||
let lengths = &mut packet_lengths[..packets];
|
||||
for length in lengths.iter_mut() {
|
||||
*length = clock.next() * frame;
|
||||
}
|
||||
if lengths.iter().any(|n| *n > self.endpoint.max_packet) {
|
||||
return Err("USB feedback exceeded endpoint capacity".into());
|
||||
}
|
||||
let total: usize = lengths.iter().sum();
|
||||
let available = total.min(s.bytes.len()) / frame * frame;
|
||||
for b in &mut slot.bytes[..available] {
|
||||
*b = s.bytes.pop_front().unwrap();
|
||||
{
|
||||
let (first, second) = s.bytes.as_slices();
|
||||
let first_len = available.min(first.len());
|
||||
slot.bytes[..first_len].copy_from_slice(&first[..first_len]);
|
||||
slot.bytes[first_len..available]
|
||||
.copy_from_slice(&second[..available - first_len]);
|
||||
}
|
||||
s.bytes.drain(..available);
|
||||
super::framing::finish_transfer(
|
||||
&mut slot.bytes[..total],
|
||||
available,
|
||||
@@ -625,7 +646,7 @@ impl Session {
|
||||
&mut marker,
|
||||
);
|
||||
slot.music_frames = (available / frame) as u64;
|
||||
slot.submit(self.handle, self.endpoint.address, &lengths)?;
|
||||
slot.submit(self.handle, self.endpoint.address, lengths)?;
|
||||
}
|
||||
}
|
||||
self.drain();
|
||||
|
||||
Reference in new issue
Block a user