/
OpusPacket.java
243 lines (223 loc) · 7.74 KB
/
OpusPacket.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
/*
* Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.dv8tion.jda.api.audio;
import net.dv8tion.jda.internal.audio.AudioPacket;
import net.dv8tion.jda.internal.audio.Decoder;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.Arrays;
import java.util.Objects;
/**
* A raw OPUS packet received from Discord that can be used for lazy decoding.
*
* @since 4.0.0
*
* @see AudioReceiveHandler#canReceiveEncoded()
* @see AudioReceiveHandler#handleEncodedAudio(OpusPacket)
*/
public final class OpusPacket implements Comparable<OpusPacket>
{
/** (Hz) We want to use the highest of qualities! All the bandwidth! */
public static final int OPUS_SAMPLE_RATE = 48000;
/** An opus frame size of 960 at 48000hz represents 20 milliseconds of audio. */
public static final int OPUS_FRAME_SIZE = 960;
/** This is 20 milliseconds. We are only dealing with 20ms opus packets. */
public static final int OPUS_FRAME_TIME_AMOUNT = 20;
/** We want to use stereo. If the audio given is mono, the encoder promotes it to Left and Right mono (stereo that is the same on both sides) */
public static final int OPUS_CHANNEL_COUNT = 2;
private final long userId;
private final byte[] opusAudio;
private final Decoder decoder;
private final AudioPacket rawPacket;
private short[] decoded;
private boolean triedDecode;
public OpusPacket(@Nonnull AudioPacket packet, long userId, @Nullable Decoder decoder)
{
this.rawPacket = packet;
this.userId = userId;
this.decoder = decoder;
this.opusAudio = packet.getEncodedAudio().array();
}
/**
* The sequence number of this packet. This is used as ordering key for {@link #compareTo(OpusPacket)}.
* <br>A char represents an unsigned short value in this case.
*
* <p>Note that packet sequence is important for decoding. If a packet is out of sequence the decode
* step will fail.
*
* @return The sequence number of this packet
*
* @see <a href="http://www.rfcreader.com/#rfc3550_line548" target="_blank">RTP Header</a>
*/
public char getSequence()
{
return rawPacket.getSequence();
}
/**
* The timestamp for this packet. As specified by the RTP header.
*
* @return The timestamp
*
* @see <a href="http://www.rfcreader.com/#rfc3550_line548" target="_blank">RTP Header</a>
*/
public int getTimestamp()
{
return rawPacket.getTimestamp();
}
/**
* The synchronization source identifier (SSRC) for the user that sent this audio packet.
*
* @return The SSRC
*
* @see <a href="http://www.rfcreader.com/#rfc3550_line548" target="_blank">RTP Header</a>
*/
public int getSSRC()
{
return rawPacket.getSSRC();
}
/**
* The ID of the responsible {@link net.dv8tion.jda.api.entities.User}.
*
* @return The user id
*/
public long getUserId()
{
return userId;
}
/**
* Whether {@link #decode()} is possible.
*
* @return True, if decode is possible.
*/
public boolean canDecode()
{
return decoder != null && decoder.isInOrder(getSequence());
}
/**
* The raw opus audio, copied to a new array.
*
* @return The raw opus audio
*/
@Nonnull
public byte[] getOpusAudio()
{
//prevent write access to backing array
return Arrays.copyOf(opusAudio, opusAudio.length);
}
/**
* Attempts to decode the opus packet.
* <br>This method is idempotent and will provide the same result on multiple calls
* without decoding again.
*
* For most use-cases {@link #getAudioData(double)} should be used instead.
*
* @throws java.lang.IllegalStateException
* If {@link #canDecode()} is false
*
* @return The decoded audio or {@code null} if decoding failed for some reason.
*
* @see #canDecode()
* @see #getAudioData(double)
*/
@Nullable
public synchronized short[] decode()
{
if (triedDecode)
return decoded;
if (decoder == null)
throw new IllegalStateException("No decoder available");
if (!decoder.isInOrder(getSequence()))
throw new IllegalStateException("Packet is not in order");
triedDecode = true;
return decoded = decoder.decodeFromOpus(rawPacket); // null if failed to decode
}
/**
* Decodes and adjusts the opus audio for the specified volume.
* <br>The provided volume should be a double precision floating point in the interval from 0 to 1.
* In this case 0.5 would represent 50% volume for instance.
*
* @param volume
* The volume
*
* @throws java.lang.IllegalArgumentException
* If {@link #decode()} returns null
*
* @return The stereo PCM audio data as specified by {@link net.dv8tion.jda.api.audio.AudioReceiveHandler#OUTPUT_FORMAT}.
*/
@Nonnull
@SuppressWarnings("ConstantConditions") // the null case is handled with an exception
public byte[] getAudioData(double volume)
{
return getAudioData(decode(), volume); // throws IllegalArgument if decode failed
}
/**
* Decodes and adjusts the opus audio for the specified volume.
* <br>The provided volume should be a double precision floating point in the interval from 0 to 1.
* In this case 0.5 would represent 50% volume for instance.
*
* @param decoded
* The decoded audio data
* @param volume
* The volume
*
* @throws java.lang.IllegalArgumentException
* If {@code decoded} is null
*
* @return The stereo PCM audio data as specified by {@link net.dv8tion.jda.api.audio.AudioReceiveHandler#OUTPUT_FORMAT}.
*/
@Nonnull
@SuppressWarnings("ConstantConditions") // the null case is handled with an exception
public static byte[] getAudioData(@Nonnull short[] decoded, double volume)
{
if (decoded == null)
throw new IllegalArgumentException("Cannot get audio data from null");
int byteIndex = 0;
byte[] audio = new byte[decoded.length * 2];
for (short s : decoded)
{
if (volume != 1.0)
s = (short) (s * volume);
byte leftByte = (byte) ((s >>> 8) & 0xFF);
byte rightByte = (byte) (s & 0xFF);
audio[byteIndex] = leftByte;
audio[byteIndex + 1] = rightByte;
byteIndex += 2;
}
return audio;
}
@Override
public int compareTo(@Nonnull OpusPacket o)
{
return getSequence() - o.getSequence();
}
@Override
public int hashCode()
{
return Objects.hash(getSequence(), getTimestamp(), getOpusAudio());
}
@Override
public boolean equals(Object obj)
{
if (obj == this)
return true;
if (!(obj instanceof OpusPacket))
return false;
OpusPacket other = (OpusPacket) obj;
return getSequence() == other.getSequence()
&& getTimestamp() == other.getTimestamp()
&& getSSRC() == other.getSSRC();
}
}