Send every ready live-capture frame per tick and cap the timer's catch-up burst

A mic running faster than the 20 ms timer filled the queue until frames were
dropped; live sources now drain each tick. The catch-up clamp was inverted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-08 20:14:42 +00:00
co-authored by Claude Opus 5.5
parent a4ce0feb61
commit 2d6f5cdf78
3 changed files with 32 additions and 18 deletions
@@ -22,6 +22,15 @@ public interface Microphone {
return 0; return 0;
} }
/**
* Whether {@link #provide} hands out frames captured on a clock of their own, such as a sound
* card's. That clock drifts against the send timer, so each tick then sends every frame that is
* ready instead of one, keeping the drift from piling up as latency or overflowing a queue.
*/
default boolean isLiveCapture() {
return false;
}
/** How many copies follow each voice packet, {@link #getRedundancyDelay} apart. */ /** How many copies follow each voice packet, {@link #getRedundancyDelay} apart. */
default int getRedundancyCopies() { default int getRedundancyCopies() {
return 1; return 1;
@@ -596,6 +596,7 @@ public class LocalTeamspeakClientSocket
new MicrophoneTask() new MicrophoneTask()
); );
microphoneThread.setDaemon(true); microphoneThread.setDaemon(true);
microphoneThread.setPriority(Thread.MAX_PRIORITY);
if (getState() != ClientConnectionState.DISCONNECTED) if (getState() != ClientConnectionState.DISCONNECTED)
microphoneThread.start(); microphoneThread.start();
@@ -693,26 +694,30 @@ public class LocalTeamspeakClientSocket
// TODO // TODO
// Set a state and send a muted event // Set a state and send a muted event
} else if (microphone.isReady()) { } else if (microphone.isReady()) {
byte[] data = microphone.provide(); boolean live = microphone.isLiveCapture();
if (data.length <= 0) byte[] data;
return; do {
data = microphone.provide();
if (data.length <= 0) break;
PacketBody0Voice voice = new PacketBody0Voice(getRole().getOut()); PacketBody0Voice voice = new PacketBody0Voice(getRole().getOut());
voice.setCodecType(microphone.getCodec()); voice.setCodecType(microphone.getCodec());
voice.setCodecData(data); voice.setCodecData(data);
if (flaggedPackets > 0) voice.setServerFlag0(sessionId); if (flaggedPackets > 0) voice.setServerFlag0(sessionId);
try { try {
writePacket(voice); writePacket(voice);
flaggedPackets--; flaggedPackets--;
wasSendingAudio = true; wasSendingAudio = true;
} catch (Exception e) { } catch (Exception e) {
// All we are really concerned about here is a disconnection event, in which case the loop // All we are really concerned about here is a disconnection event, in which case the loop
// will exit. // will exit.
getExceptionHandler().accept(e); getExceptionHandler().accept(e);
} return;
}
} while (live && microphone.isReady());
} else if (wasSendingAudio) { } else if (wasSendingAudio) {
PacketBody0Voice voice = new PacketBody0Voice(getRole().getOut()); PacketBody0Voice voice = new PacketBody0Voice(getRole().getOut());
@@ -64,8 +64,8 @@ public class HighPrecisionTimer extends Thread {
now = System.nanoTime(); now = System.nanoTime();
// Clamp the next target to the maximum "drift" allowed // After a stall, catch up at most the allowed number of ticks rather than all of them
if (nextTarget - now > maximumDriftInNanos) if (now - nextTarget > maximumDriftInNanos)
nextTarget = now - maximumDriftInNanos; nextTarget = now - maximumDriftInNanos;
if (sleepFor(nextTarget - now)) { if (sleepFor(nextTarget - now)) {