perf(usb): reuse PCM buffers and respect native ring capacity

This commit is contained in:
zarzet committed 2026-10-04 01:40:59 +07:00
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();