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());
|
channelIds[n] = (int) (buffer.getLong());
|
||||||
|
|
||||||
clientIds = new int[clientIdCount];
|
clientIds = new int[clientIdCount];
|
||||||
for (int n = 0; n < channelIdCount; n ++)
|
for (int n = 0; n < clientIdCount; n ++)
|
||||||
clientIds[n] = (buffer.getShort() & 0xFFFF);
|
clientIds[n] = (buffer.getShort() & 0xFFFF);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getSize() {
|
public int getSize() {
|
||||||
return 2 + 2 +
|
return 1 + 1 +
|
||||||
(channelIds == null ? 0 : channelIds.length * 8) +
|
(channelIds == null ? 0 : channelIds.length * 8) +
|
||||||
(clientIds == null ? 0 : clientIds.length * 2);
|
(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
|
// > 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.
|
// 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());
|
((PacketBody0Voice)packet.getBody()).setPacketId(header.getPacketId());
|
||||||
|
} else if (header.getType() == PacketBodyType.VOICE_WHISPER) {
|
||||||
|
((PacketBody1VoiceWhisper)packet.getBody()).setPacketId(header.getPacketId());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Flush to a buffer
|
// Flush to a buffer
|
||||||
|
|||||||
+53
-13
@@ -16,7 +16,10 @@ import com.github.manevolent.ts3j.identity.Uid;
|
|||||||
import com.github.manevolent.ts3j.protocol.*;
|
import com.github.manevolent.ts3j.protocol.*;
|
||||||
import com.github.manevolent.ts3j.protocol.client.ClientConnectionState;
|
import com.github.manevolent.ts3j.protocol.client.ClientConnectionState;
|
||||||
import com.github.manevolent.ts3j.protocol.header.PacketHeader;
|
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.PacketBody0Voice;
|
||||||
|
import com.github.manevolent.ts3j.protocol.packet.PacketBody1VoiceWhisper;
|
||||||
import com.github.manevolent.ts3j.protocol.packet.PacketBody2Command;
|
import com.github.manevolent.ts3j.protocol.packet.PacketBody2Command;
|
||||||
import com.github.manevolent.ts3j.protocol.packet.PacketBodyType;
|
import com.github.manevolent.ts3j.protocol.packet.PacketBodyType;
|
||||||
import com.github.manevolent.ts3j.protocol.packet.handler.PacketHandler;
|
import com.github.manevolent.ts3j.protocol.packet.handler.PacketHandler;
|
||||||
@@ -54,6 +57,7 @@ public class LocalTeamspeakClientSocket
|
|||||||
private volatile InetSocketAddress remote;
|
private volatile InetSocketAddress remote;
|
||||||
|
|
||||||
private Microphone microphone;
|
private Microphone microphone;
|
||||||
|
private volatile PacketBody1VoiceWhisper.WhisperTarget whisperTarget;
|
||||||
private HighPrecisionTimer microphoneThread;
|
private HighPrecisionTimer microphoneThread;
|
||||||
/** Voice packets still to be sent again, soonest first; see {@link Microphone#getRedundancyDelay}. */
|
/** Voice packets still to be sent again, soonest first; see {@link Microphone#getRedundancyDelay}. */
|
||||||
private final Queue<VoiceCopy> voiceCopies =
|
private final Queue<VoiceCopy> voiceCopies =
|
||||||
@@ -286,7 +290,8 @@ public class LocalTeamspeakClientSocket
|
|||||||
socket.send(packet.getDatagram());
|
socket.send(packet.getDatagram());
|
||||||
|
|
||||||
long copyDelay = voiceCopyDelayNanos;
|
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.
|
// 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;
|
long sent = System.nanoTime() - TICK_MS * 1_000_000L / 2;
|
||||||
for (int i = 1, n = voiceCopyCount; i <= n; i++)
|
for (int i = 1, n = voiceCopyCount; i <= n; i++)
|
||||||
@@ -585,6 +590,19 @@ public class LocalTeamspeakClientSocket
|
|||||||
return microphone;
|
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) {
|
public void setMicrophone(Microphone microphone) {
|
||||||
if (this.microphone != microphone) {
|
if (this.microphone != microphone) {
|
||||||
this.microphone = microphone;
|
this.microphone = microphone;
|
||||||
@@ -664,6 +682,8 @@ public class LocalTeamspeakClientSocket
|
|||||||
|
|
||||||
private class MicrophoneTask implements Runnable {
|
private class MicrophoneTask implements Runnable {
|
||||||
private boolean wasSendingAudio = false;
|
private boolean wasSendingAudio = false;
|
||||||
|
/** Where the voice session under way goes; null for the channel. */
|
||||||
|
private PacketBody1VoiceWhisper.WhisperTarget sendingTarget;
|
||||||
private byte sessionId = 1;
|
private byte sessionId = 1;
|
||||||
private int flaggedPackets = 5;
|
private int flaggedPackets = 5;
|
||||||
|
|
||||||
@@ -690,6 +710,12 @@ public class LocalTeamspeakClientSocket
|
|||||||
voiceCopyCount = copies;
|
voiceCopyCount = copies;
|
||||||
voiceCopyDelayNanos = copyDelay > 0 && copies > 0 ? copyDelay * 1_000_000L : 0;
|
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()) {
|
if (microphone.isMuted()) {
|
||||||
// TODO
|
// TODO
|
||||||
// Set a state and send a muted event
|
// Set a state and send a muted event
|
||||||
@@ -700,17 +726,13 @@ public class LocalTeamspeakClientSocket
|
|||||||
data = microphone.provide();
|
data = microphone.provide();
|
||||||
if (data.length <= 0) break;
|
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 {
|
try {
|
||||||
writePacket(voice);
|
writePacket(voicePacket(target, microphone.getCodec(), data,
|
||||||
|
flaggedPackets > 0 && target == null ? sessionId : null));
|
||||||
|
|
||||||
flaggedPackets--;
|
flaggedPackets--;
|
||||||
wasSendingAudio = true;
|
wasSendingAudio = true;
|
||||||
|
sendingTarget = target;
|
||||||
} 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.
|
||||||
@@ -719,13 +741,14 @@ public class LocalTeamspeakClientSocket
|
|||||||
}
|
}
|
||||||
} while (live && microphone.isReady());
|
} while (live && microphone.isReady());
|
||||||
} else if (wasSendingAudio) {
|
} else if (wasSendingAudio) {
|
||||||
PacketBody0Voice voice = new PacketBody0Voice(getRole().getOut());
|
sendEnd(microphone);
|
||||||
|
}
|
||||||
voice.setCodecType(microphone.getCodec());
|
}
|
||||||
voice.setCodecData(new byte[0]);
|
|
||||||
|
|
||||||
|
/** Ends the voice session on the route it went, which an empty packet does; false once disconnected. */
|
||||||
|
private boolean sendEnd(Microphone microphone) {
|
||||||
try {
|
try {
|
||||||
writePacket(voice);
|
writePacket(voicePacket(sendingTarget, microphone.getCodec(), new byte[0], null));
|
||||||
|
|
||||||
// reset decoder flag
|
// reset decoder flag
|
||||||
flaggedPackets = 5;
|
flaggedPackets = 5;
|
||||||
@@ -735,12 +758,29 @@ public class LocalTeamspeakClientSocket
|
|||||||
sessionId = 1;
|
sessionId = 1;
|
||||||
|
|
||||||
wasSendingAudio = false;
|
wasSendingAudio = false;
|
||||||
|
return 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 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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