Skip to content

Commit

Permalink
Change handling of speaking updates in voice connections (#2240)
Browse files Browse the repository at this point in the history
And deprecate setSpeakingDelay
  • Loading branch information
MinnDevelopment committed Jan 16, 2023
1 parent 83ae997 commit 52d941e
Show file tree
Hide file tree
Showing 4 changed files with 26 additions and 90 deletions.
Expand Up @@ -77,53 +77,34 @@ public interface IPacketProvider

/**
* Used to retrieve an audio packet to send to Discord. The packet provided is already converted to Opus and
* encrypted, and as such is completely ready to be sent to Discord. The {@code changeTalking} parameter is used
* to control whether or not the talking indicator should be changed if the
* {@link net.dv8tion.jda.api.audio.AudioSendHandler AudioSendHandler} cannot provide an audio packet.
* <br>The {@link java.nio.ByteBuffer#position()} will be positioned on the start of the packet to send
* encrypted, and as such is completely ready to be sent to Discord.
*
* <p>The {@link java.nio.ByteBuffer#position()} will be positioned on the start of the packet to send
* and the {@link java.nio.ByteBuffer#limit()} at the end of it. Use {@link java.nio.ByteBuffer#remaining()}
* to check the length of the packet.
*
* <p>Use case for this parameter would be front-loading or queuing many audio packets ahead of send time, and if the AudioSendHandler
* did not have enough to fill the entire queue, you would have {@code changeTalking} set to {@code false} until the queue
* was empty. At that point, you would switch to {@code true} when requesting a new packet due to the fact that if
* one was not available, the developer would not have a packet to send, thus the logged in account is no longer "talking".
*
* <p><b>Note:</b> When the AudioSendHandler cannot or does not provide a new packet to send, this method will return null.
*
* <p><u>The buffer used here may be used again on the next call to this getter, if you plan on storing the data copy it.
* <p><u>The buffer used here may be used again on the next call to this getter, if you plan on storing the data, copy it.
* The buffer was created using {@link ByteBuffer#allocate(int)} and is not direct.</u>
*
* @param changeTalking
* Whether or not to change the talking indicator if the AudioSendHandler cannot provide a new audio packet.
*
* @return Possibly-null {@link ByteBuffer} containing an encoded and encrypted packet
* of audio data ready to be sent to discord.
*/
@Nullable
ByteBuffer getNextPacketRaw(boolean changeTalking);
ByteBuffer getNextPacketRaw(boolean unused);

/**
* Used to retrieve an audio packet to send to Discord. The packet provided is already converted to Opus and
* encrypted, and as such is completely ready to be sent to Discord. The {@code changeTalking} parameter is used
* to control whether or not the talking indicator should be changed if the
* {@link net.dv8tion.jda.api.audio.AudioSendHandler AudioSendHandler} cannot provide an audio packet.
*
* <p>Use case for this parameter would be front-loading or queuing many audio packets ahead of send time, and if the AudioSendHandler
* did not have enough to fill the entire queue, you would have {@code changeTalking} set to {@code false} until the queue
* was empty. At that point, you would switch to {@code true} when requesting a new packet due to the fact that if
* one was not available, the developer would not have a packet to send, thus the logged in account is no longer "talking".
* encrypted, and as such is completely ready to be sent to Discord.
*
* <p><b>Note:</b> When the AudioSendHandler cannot or does not provide a new packet to send, this method will return null.
*
* @param changeTalking
* Whether or not to change the talking indicator if the AudioSendHandler cannot provide a new audio packet.
*
* @return Possibly-null {@link java.net.DatagramPacket DatagramPacket} containing an encoded and encrypted packet
* of audio data ready to be sent to discord.
*/
@Nullable
DatagramPacket getNextPacket(boolean changeTalking);
DatagramPacket getNextPacket(boolean unused);

/**
* This method is used to indicate a connection error to JDA so that the connection can be properly shutdown.
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/net/dv8tion/jda/api/managers/AudioManager.java
Expand Up @@ -16,6 +16,7 @@

package net.dv8tion.jda.api.managers;

import net.dv8tion.jda.annotations.ForRemoval;
import net.dv8tion.jda.annotations.Incubating;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.audio.AudioReceiveHandler;
Expand Down Expand Up @@ -158,6 +159,8 @@ default void setSpeakingMode(@Nonnull SpeakingMode... mode)
*
* @since 4.0.0
*/
@Deprecated
@ForRemoval
void setSpeakingDelay(int millis);

/**
Expand Down
74 changes: 15 additions & 59 deletions src/main/java/net/dv8tion/jda/internal/audio/AudioConnection.java
Expand Up @@ -56,7 +56,6 @@ public class AudioConnection

public static final long MAX_UINT_32 = 4294967295L;

private static final int NOT_SPEAKING = 0;
private static final ByteBuffer silenceBytes = ByteBuffer.wrap(new byte[] {(byte)0xF8, (byte)0xFF, (byte)0xFE});
private static boolean printedError = false;

Expand All @@ -75,16 +74,12 @@ public class AudioConnection
private IAudioSendSystem sendSystem;
private Thread receiveThread;
private long queueTimeout;
private boolean sentSilenceOnConnect = false;
private int speakingDelay = 10;

private volatile AudioSendHandler sendHandler = null;
private volatile AudioReceiveHandler receiveHandler = null;

private volatile boolean couldReceive = false;
private volatile boolean speaking = false; //Also acts as "couldProvide"
private volatile int speakingMode = SpeakingMode.VOICE.getRaw();
private volatile int silenceCounter = 0;

public AudioConnection(AudioManagerImpl manager, String endpoint, String sessionId, String token, AudioChannel channel)
{
Expand Down Expand Up @@ -112,9 +107,9 @@ public void setAutoReconnect(boolean shouldReconnect)
webSocket.setAutoReconnect(shouldReconnect);
}

@Deprecated
public void setSpeakingDelay(int millis)
{
speakingDelay = Math.max(millis / 20, 10); // max { millis / frame-length, 200 millis }
}

public void setSendingHandler(AudioSendHandler handler)
Expand All @@ -134,7 +129,7 @@ public void setReceivingHandler(AudioReceiveHandler handler)
public void setSpeakingMode(EnumSet<SpeakingMode> mode)
{
int raw = SpeakingMode.getRaw(mode);
if (raw != this.speakingMode && speaking)
if (raw != this.speakingMode && webSocket.isReady())
setSpeaking(raw);
this.speakingMode = raw;
}
Expand Down Expand Up @@ -212,7 +207,7 @@ protected void prepareReady()
final long timeout = getGuild().getAudioManager().getConnectTimeout();

final long started = System.currentTimeMillis();
while (!webSocket.isReady())
while (!webSocket.isReady()) // TODO: This should use a conditional variable
{
if (timeout > 0 && System.currentTimeMillis() - started > timeout)
break;
Expand Down Expand Up @@ -295,6 +290,7 @@ private synchronized void setupSendSystem()
{
if (udpSocket != null && !udpSocket.isClosed() && sendHandler != null && sendSystem == null)
{
setSpeaking(speakingMode);
IAudioSendFactory factory = getJDA().getAudioSendFactory();
sendSystem = factory.createSendSystem(new PacketProvider(new TweetNaclFast.SecretBox(webSocket.getSecretKey())));
sendSystem.setContextMap(getJDA().getContextMap());
Expand Down Expand Up @@ -366,11 +362,7 @@ private synchronized void setupReceiveThread()
boolean canReceive = receiveHandler != null && (receiveHandler.canReceiveUser() || receiveHandler.canReceiveCombined() || receiveHandler.canReceiveEncoded());
if (canReceive && webSocket.getSecretKey() != null)
{
if (!couldReceive)
{
couldReceive = true;
sendSilentPackets();
}
couldReceive = true;
AudioPacket decryptedPacket = AudioPacket.decryptAudioPacket(webSocket.encryption, receivedPacket, webSocket.getSecretKey());
if (decryptedPacket == null)
continue;
Expand Down Expand Up @@ -435,10 +427,9 @@ else if (!receiveHandler.canReceiveEncoded())
queue.add(new AudioData(decodedAudio));
}
}
else if (couldReceive)
else
{
couldReceive = false;
sendSilentPackets();
}
}
catch (SocketTimeoutException e)
Expand Down Expand Up @@ -593,18 +584,13 @@ private ByteBuffer encodeToOpus(ByteBuffer rawAudio)

private void setSpeaking(int raw)
{
this.speaking = raw != 0;
DataObject obj = DataObject.empty()
.put("speaking", raw)
.put("ssrc", webSocket.getSSRC())
.put("delay", 0);
webSocket.send(VoiceCode.USER_SPEAKING_UPDATE, obj);
}

private void sendSilentPackets()
{
silenceCounter = 0;
}

@Override
@SuppressWarnings("deprecation") /* If this was in JDK9 we would be using java.lang.ref.Cleaner instead! */
Expand All @@ -615,13 +601,13 @@ protected void finalize()

private class PacketProvider implements IPacketProvider
{
private final TweetNaclFast.SecretBox boxer;
private final byte[] nonceBuffer = new byte[TweetNaclFast.SecretBox.nonceLength];
private char seq = 0; //Sequence of audio packets. Used to determine the order of the packets.
private int timestamp = 0; //Used to sync up our packets within the same timeframe of other people talking.
private TweetNaclFast.SecretBox boxer;
private long nonce = 0;
private ByteBuffer buffer = ByteBuffer.allocate(512);
private ByteBuffer encryptionBuffer = ByteBuffer.allocate(512);
private final byte[] nonceBuffer = new byte[TweetNaclFast.SecretBox.nonceLength];

public PacketProvider(TweetNaclFast.SecretBox boxer)
{
Expand Down Expand Up @@ -657,74 +643,44 @@ public InetSocketAddress getSocketAddress()
}

@Override
public DatagramPacket getNextPacket(boolean changeTalking)
public DatagramPacket getNextPacket(boolean unused)
{
ByteBuffer buffer = getNextPacketRaw(changeTalking);
ByteBuffer buffer = getNextPacketRaw(unused);
return buffer == null ? null : getDatagramPacket(buffer);
}

@Override
public ByteBuffer getNextPacketRaw(boolean changeTalking)
public ByteBuffer getNextPacketRaw(boolean unused)
{
ByteBuffer nextPacket = null;
try
{
cond: if (sentSilenceOnConnect && sendHandler != null && sendHandler.canProvide())
if (sendHandler != null && sendHandler.canProvide())
{
silenceCounter = -1;
ByteBuffer rawAudio = sendHandler.provide20MsAudio();
if (rawAudio != null && !rawAudio.hasArray())
{
// we can't use the boxer without an array so encryption would not work
LOG.error("AudioSendHandler provided ByteBuffer without a backing array! This is unsupported.");
}
if (rawAudio == null || !rawAudio.hasRemaining() || !rawAudio.hasArray())
{
if (speaking && changeTalking)
sendSilentPackets();
}
else

if (rawAudio != null && rawAudio.hasRemaining() && rawAudio.hasArray())
{
if (!sendHandler.isOpus())
{
rawAudio = encodeAudio(rawAudio);
if (rawAudio == null)
break cond;
return null;
}

nextPacket = getPacketData(rawAudio);
if (!speaking)
setSpeaking(speakingMode);

if (seq + 1 > Character.MAX_VALUE)
seq = 0;
else
seq++;
}
}
else if (silenceCounter > -1)
{
nextPacket = getPacketData(silenceBytes);
if (seq + 1 > Character.MAX_VALUE)
seq = 0;
else
seq++;

silenceCounter++;
//If we have sent our 10 silent packets on initial connect, or if we have sent enough silent packets
// to satisfy the speaking delay, stop transmitting silence.
if ((!sentSilenceOnConnect && silenceCounter > 10) || silenceCounter > speakingDelay)
{
if (sentSilenceOnConnect)
setSpeaking(NOT_SPEAKING);
silenceCounter = -1;
sentSilenceOnConnect = true;
}
}
else if (speaking && changeTalking)
{
sendSilentPackets();
}
}
catch (Exception e)
{
Expand Down
Expand Up @@ -58,7 +58,6 @@ public class AudioManagerImpl implements AudioManager
protected boolean selfDeafened = false;

protected long timeout = DEFAULT_CONNECTION_TIMEOUT;
protected int speakingDelay = 0;

public AudioManagerImpl(GuildImpl guild)
{
Expand Down Expand Up @@ -156,11 +155,9 @@ public EnumSet<SpeakingMode> getSpeakingMode()
}

@Override
@Deprecated
public void setSpeakingDelay(int millis)
{
this.speakingDelay = millis;
if (audioConnection != null)
audioConnection.setSpeakingDelay(millis);
}

@Nonnull
Expand Down Expand Up @@ -319,7 +316,6 @@ public void setAudioConnection(AudioConnection audioConnection)
audioConnection.setReceivingHandler(receiveHandler);
audioConnection.setQueueTimeout(queueTimeout);
audioConnection.setSpeakingMode(speakingModes);
audioConnection.setSpeakingDelay(speakingDelay);
}

public void setConnectedChannel(AudioChannel channel)
Expand Down

0 comments on commit 52d941e

Please sign in to comment.