Send the microphone's voice as whispers to a target
setWhisperTarget routes voice into VOICE_WHISPER packets to channels and clients until cleared; a voice session under way ends on its old route first. Whisper packets carry their own packet id and get the redundant copies voice does. WhisperTargetMultiple sized its two counts as two bytes each, leaving zero bytes after the voice data, and read the client ids by the channel count. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+2
-2
@@ -239,13 +239,13 @@ public class PacketBody1VoiceWhisper extends PacketBody {
|
||||
channelIds[n] = (int) (buffer.getLong());
|
||||
|
||||
clientIds = new int[clientIdCount];
|
||||
for (int n = 0; n < channelIdCount; n ++)
|
||||
for (int n = 0; n < clientIdCount; n ++)
|
||||
clientIds[n] = (buffer.getShort() & 0xFFFF);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSize() {
|
||||
return 2 + 2 +
|
||||
return 1 + 1 +
|
||||
(channelIds == null ? 0 : channelIds.length * 8) +
|
||||
(clientIds == null ? 0 : clientIds.length * 2);
|
||||
}
|
||||
|
||||
+2
@@ -478,6 +478,8 @@ public abstract class AbstractTeamspeakClientSocket
|
||||
// > X is a ushort in H2N order of an own audio packet counter
|
||||
// it seems it can be the same as the packet counter so we will let the packethandler do it.
|
||||
((PacketBody0Voice)packet.getBody()).setPacketId(header.getPacketId());
|
||||
} else if (header.getType() == PacketBodyType.VOICE_WHISPER) {
|
||||
((PacketBody1VoiceWhisper)packet.getBody()).setPacketId(header.getPacketId());
|
||||
}
|
||||
|
||||
// Flush to a buffer
|
||||
|
||||
+69
-29
@@ -16,7 +16,10 @@ import com.github.manevolent.ts3j.identity.Uid;
|
||||
import com.github.manevolent.ts3j.protocol.*;
|
||||
import com.github.manevolent.ts3j.protocol.client.ClientConnectionState;
|
||||
import com.github.manevolent.ts3j.protocol.header.PacketHeader;
|
||||
import com.github.manevolent.ts3j.enums.CodecType;
|
||||
import com.github.manevolent.ts3j.protocol.packet.PacketBody;
|
||||
import com.github.manevolent.ts3j.protocol.packet.PacketBody0Voice;
|
||||
import com.github.manevolent.ts3j.protocol.packet.PacketBody1VoiceWhisper;
|
||||
import com.github.manevolent.ts3j.protocol.packet.PacketBody2Command;
|
||||
import com.github.manevolent.ts3j.protocol.packet.PacketBodyType;
|
||||
import com.github.manevolent.ts3j.protocol.packet.handler.PacketHandler;
|
||||
@@ -54,6 +57,7 @@ public class LocalTeamspeakClientSocket
|
||||
private volatile InetSocketAddress remote;
|
||||
|
||||
private Microphone microphone;
|
||||
private volatile PacketBody1VoiceWhisper.WhisperTarget whisperTarget;
|
||||
private HighPrecisionTimer microphoneThread;
|
||||
/** Voice packets still to be sent again, soonest first; see {@link Microphone#getRedundancyDelay}. */
|
||||
private final Queue<VoiceCopy> voiceCopies =
|
||||
@@ -286,7 +290,8 @@ public class LocalTeamspeakClientSocket
|
||||
socket.send(packet.getDatagram());
|
||||
|
||||
long copyDelay = voiceCopyDelayNanos;
|
||||
if (copyDelay > 0 && packet.getHeader().getType() == PacketBodyType.VOICE) {
|
||||
PacketBodyType type = packet.getHeader().getType();
|
||||
if (copyDelay > 0 && (type == PacketBodyType.VOICE || type == PacketBodyType.VOICE_WHISPER)) {
|
||||
// Aim half a tick early, so each copy goes out on the tick nearest its time.
|
||||
long sent = System.nanoTime() - TICK_MS * 1_000_000L / 2;
|
||||
for (int i = 1, n = voiceCopyCount; i <= n; i++)
|
||||
@@ -585,6 +590,19 @@ public class LocalTeamspeakClientSocket
|
||||
return microphone;
|
||||
}
|
||||
|
||||
/** Where the microphone's voice goes: null for our channel, else whisper packets to the target. */
|
||||
public PacketBody1VoiceWhisper.WhisperTarget getWhisperTarget() {
|
||||
return whisperTarget;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the microphone's voice to {@code target} as whispers, or to our channel again with null.
|
||||
* A voice session under way ends on its old route first.
|
||||
*/
|
||||
public void setWhisperTarget(PacketBody1VoiceWhisper.WhisperTarget target) {
|
||||
this.whisperTarget = target;
|
||||
}
|
||||
|
||||
public void setMicrophone(Microphone microphone) {
|
||||
if (this.microphone != microphone) {
|
||||
this.microphone = microphone;
|
||||
@@ -664,6 +682,8 @@ public class LocalTeamspeakClientSocket
|
||||
|
||||
private class MicrophoneTask implements Runnable {
|
||||
private boolean wasSendingAudio = false;
|
||||
/** Where the voice session under way goes; null for the channel. */
|
||||
private PacketBody1VoiceWhisper.WhisperTarget sendingTarget;
|
||||
private byte sessionId = 1;
|
||||
private int flaggedPackets = 5;
|
||||
|
||||
@@ -690,6 +710,12 @@ public class LocalTeamspeakClientSocket
|
||||
voiceCopyCount = copies;
|
||||
voiceCopyDelayNanos = copyDelay > 0 && copies > 0 ? copyDelay * 1_000_000L : 0;
|
||||
|
||||
PacketBody1VoiceWhisper.WhisperTarget target = whisperTarget;
|
||||
if (wasSendingAudio && target != sendingTarget) {
|
||||
// The listeners of the old route hear the end of what they were hearing.
|
||||
if (!sendEnd(microphone)) return;
|
||||
}
|
||||
|
||||
if (microphone.isMuted()) {
|
||||
// TODO
|
||||
// Set a state and send a muted event
|
||||
@@ -700,17 +726,13 @@ public class LocalTeamspeakClientSocket
|
||||
data = microphone.provide();
|
||||
if (data.length <= 0) break;
|
||||
|
||||
PacketBody0Voice voice = new PacketBody0Voice(getRole().getOut());
|
||||
|
||||
voice.setCodecType(microphone.getCodec());
|
||||
voice.setCodecData(data);
|
||||
if (flaggedPackets > 0) voice.setServerFlag0(sessionId);
|
||||
|
||||
try {
|
||||
writePacket(voice);
|
||||
writePacket(voicePacket(target, microphone.getCodec(), data,
|
||||
flaggedPackets > 0 && target == null ? sessionId : null));
|
||||
|
||||
flaggedPackets--;
|
||||
wasSendingAudio = true;
|
||||
sendingTarget = target;
|
||||
} catch (Exception e) {
|
||||
// All we are really concerned about here is a disconnection event, in which case the loop
|
||||
// will exit.
|
||||
@@ -719,29 +741,47 @@ public class LocalTeamspeakClientSocket
|
||||
}
|
||||
} while (live && microphone.isReady());
|
||||
} else if (wasSendingAudio) {
|
||||
PacketBody0Voice voice = new PacketBody0Voice(getRole().getOut());
|
||||
|
||||
voice.setCodecType(microphone.getCodec());
|
||||
voice.setCodecData(new byte[0]);
|
||||
|
||||
try {
|
||||
writePacket(voice);
|
||||
|
||||
// reset decoder flag
|
||||
flaggedPackets = 5;
|
||||
sessionId++;
|
||||
|
||||
if (sessionId >= 8)
|
||||
sessionId = 1;
|
||||
|
||||
wasSendingAudio = false;
|
||||
} catch (Exception e) {
|
||||
// All we are really concerned about here is a disconnection event, in which case the loop
|
||||
// will exit.
|
||||
getExceptionHandler().accept(e);
|
||||
}
|
||||
sendEnd(microphone);
|
||||
}
|
||||
}
|
||||
|
||||
/** Ends the voice session on the route it went, which an empty packet does; false once disconnected. */
|
||||
private boolean sendEnd(Microphone microphone) {
|
||||
try {
|
||||
writePacket(voicePacket(sendingTarget, microphone.getCodec(), new byte[0], null));
|
||||
|
||||
// reset decoder flag
|
||||
flaggedPackets = 5;
|
||||
sessionId++;
|
||||
|
||||
if (sessionId >= 8)
|
||||
sessionId = 1;
|
||||
|
||||
wasSendingAudio = false;
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
// All we are really concerned about here is a disconnection event, in which case the loop
|
||||
// will exit.
|
||||
getExceptionHandler().accept(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private PacketBody voicePacket(PacketBody1VoiceWhisper.WhisperTarget target, CodecType codec, byte[] data,
|
||||
Byte flag) {
|
||||
if (target == null) {
|
||||
PacketBody0Voice voice = new PacketBody0Voice(getRole().getOut());
|
||||
voice.setCodecType(codec);
|
||||
voice.setCodecData(data);
|
||||
if (flag != null) voice.setServerFlag0(flag);
|
||||
return voice;
|
||||
}
|
||||
PacketBody1VoiceWhisper whisper = new PacketBody1VoiceWhisper(getRole().getOut());
|
||||
whisper.setCodecType(codec);
|
||||
whisper.setTarget(target);
|
||||
whisper.setCodecData(data);
|
||||
return whisper;
|
||||
}
|
||||
}
|
||||
|
||||
private class ClientCommandResponse<T>
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.github.manevolent.ts3j;
|
||||
|
||||
import com.github.manevolent.ts3j.enums.CodecType;
|
||||
import com.github.manevolent.ts3j.protocol.ProtocolRole;
|
||||
import com.github.manevolent.ts3j.protocol.header.ClientPacketHeader;
|
||||
import com.github.manevolent.ts3j.protocol.packet.PacketBody1VoiceWhisper;
|
||||
import junit.framework.TestCase;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
|
||||
/** A whisper a client sends: codec, then the channel and client counts and ids, then the voice. */
|
||||
public class WhisperPacketTest extends TestCase {
|
||||
|
||||
public void testClientWhisperRoundTrip() {
|
||||
PacketBody1VoiceWhisper.WhisperTargetMultiple target = new PacketBody1VoiceWhisper.WhisperTargetMultiple();
|
||||
target.setChannelIds(new int[]{7, 9});
|
||||
target.setClientIds(new int[]{3});
|
||||
PacketBody1VoiceWhisper sent = new PacketBody1VoiceWhisper(ProtocolRole.CLIENT);
|
||||
sent.setPacketId(42);
|
||||
sent.setCodecType(CodecType.OPUS_VOICE);
|
||||
sent.setTarget(target);
|
||||
sent.setCodecData(new byte[]{1, 2, 3});
|
||||
|
||||
ByteBuffer buffer = ByteBuffer.allocate(sent.getSize());
|
||||
sent.write(buffer);
|
||||
assertEquals("the size covers exactly what is written", buffer.capacity(), buffer.position());
|
||||
assertEquals(2 + 1 + 1 + 1 + 2 * 8 + 2 + 3, buffer.capacity());
|
||||
|
||||
buffer.flip();
|
||||
PacketBody1VoiceWhisper read = new PacketBody1VoiceWhisper(ProtocolRole.CLIENT);
|
||||
read.read(new ClientPacketHeader(), buffer);
|
||||
PacketBody1VoiceWhisper.WhisperTargetMultiple readTarget =
|
||||
(PacketBody1VoiceWhisper.WhisperTargetMultiple) read.getTarget();
|
||||
assertEquals(42, read.getPacketId());
|
||||
assertEquals(CodecType.OPUS_VOICE, read.getCodecType());
|
||||
assertTrue(Arrays.equals(new int[]{7, 9}, readTarget.getChannelIds()));
|
||||
assertTrue(Arrays.equals(new int[]{3}, readTarget.getClientIds()));
|
||||
assertTrue(Arrays.equals(new byte[]{1, 2, 3}, read.getCodecData()));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user