New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Use ByteBuffer for sending audio #921
Conversation
Both receive and send seem to be working as expected after these changes. This implies encoding/decoding of opus and encryption of audio are unaffected and work just like before. The conversion for v3 users is actually a one-liner: - return data;
+ return data == null ? null : ByteBuffer.wrap(data); |
We need an array in order to properly encrypt the packet later on
There is no need to create a new secret box for the same key every 20 ms
This fixes the position pointer of the buffer which was previously just pointing at the write position rather than the read position.
Can you provide a full example of a v3 vs v4 implementation of AudioSendHandler? Also, why does this not impact the AudioReceiveHandler? |
Its exactly the same as v3 except that private final MutableAudioFrame frame = new MutableAudioFrame();
private final ByteBuffer buffer = ByteBuffer.allocate(1024);
public MyConstructor() {
this.frame.setBuffer(buffer);
}
public ByteBuffer provide20MsAudio() {
return buffer.flip();
}
public boolean canProvide() {
return player.provide(frame);
} The difference is that in v3 you have to return a fitting byte array instead which requires re-allocation for every frame.
Because audio receive is not affected by changes to the send system. |
Let me rephrase: Would there be any benefit we could eke out by using a buffer of some sort for the receiving system as well? |
The receive system is generally not very memory optimized. Additionally, I only want to make changes to receive required to keep it working for now. |
Currently, lavaplayer might change the buffer in MutableAudioFrame, so a safer approach for the send handler would be something like this (reflection is needed because there is no other way of knowing whether or not the buffer was replaced) public class MySendHandler implements AudioSendHandler {
private static final Function<MutableAudioFrame, ByteBuffer> BUFFER_GETTER;
static {
try {
Field f = MutableAudioFrame.class.getDeclaredField("frameBuffer");
f.setAccessible(true);
BUFFER_GETTER = frame -> {
try {
return (ByteBuffer)f.get(frame);
} catch(Exception impossible) {
throw new AssertionError(impossible);
}
};
} catch(Exception e) {
throw new ExceptionInInitializerError(e);
}
}
//other fields and methods
@Override
public ByteBuffer provide20MsAudio() {
if(BUFFER_GETTER.apply(frame) != buffer) {
frame.getData(buffer.array(), frame.getDataLength());
frame.setBuffer(buffer.limit(buffer.capacity()));
}
return buffer.position(0).limit(frame.getDataLength());
}
} |
@natanbc can't you just use |
That would work as well, but would copy the data every time, this approach only copies when the buffer is replaced. After setting the buffer to the original one, new data will be written to it until LP changes the buffer again |
It would be ideal if |
In any case, this doesn't really matter for our interface change. We only need to provide a way to re-use one buffer properly. That should be possible regardless of lavaplayer usage. |
src/main/java/net/dv8tion/jda/internal/audio/AudioConnection.java
Outdated
Show resolved
Hide resolved
The AudioEcho example is a quality addition. Nice work. |
LGTM |
*/ | ||
byte[] provide20MsAudio(); | ||
ByteBuffer provide20MsAudio(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this method have a ByteBuffer as an argument that the audio data should be written to?
Returning a buffer looks off because how does the implementor know when the buffer it returns here is safe to be used again? Or am I on the wrong track about how this would be used in an implementation with the goal of minimal allocations?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you would do provide(ByteBuffer) you get additional overhead for copying over data. It would also severely limit usability since the buffer could not be re-allocated correctly. The buffer should be updated by the next provide cycle. Meaning canProvide signals the buffer was sent and the next one will be requested. If you are worried about concurrency you can always use buffer.duplicate()
instead or have a queue of buffers. Since we immediately read the buffer and write the encrypted data into a different buffer of the packet provider it should almost immediately be usable again.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Additionally, the goal of these changes was not to force users into single-allocation which your suggestion would imply. The goal was to make it possible in the first place. Since returning a buffer with a position and limit you can create a fitting requirement without re-allocation compared to using a byte[]. This is due to the fact that byte[] needs to be allocated with the exact size needed for the audio frame while the buffer can be adjusted for it without any allocations. To achieve 0 allocations you simply need to have one buffer that is filled by canProvide() and then returned by provide() as was shown by the examples in the comments of this PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for clarifying!
Pull Request Etiquette
Changes
Closes Issue: NaN
Description
This change allows send handlers to re-use a single buffer for all packets.
Example Usage