Skip to content

Commit

Permalink
Adds support for mono channel audio files
Browse files Browse the repository at this point in the history
  • Loading branch information
austinv11 committed Mar 17, 2016
1 parent 4579561 commit 25cb0c2
Show file tree
Hide file tree
Showing 8 changed files with 88 additions and 35 deletions.
2 changes: 0 additions & 2 deletions .gitignore
Expand Up @@ -21,5 +21,3 @@ dependency-reduced-pom.xml
Roadmap.md
modules/
bin/
# Blame copyright laws
test2.mp3
2 changes: 1 addition & 1 deletion pom.xml
Expand Up @@ -6,7 +6,7 @@

<groupId>sx.blah</groupId>
<artifactId>Discord4J</artifactId>
<version>2.3.2</version>
<version>2.3.3-SNAPSHOT</version>
<description>A Java binding for the unofficial Discord API, forked from https://github.com/nerd/Discord4J. Copyright (c) 2016, Licensed under GNU GPLv2</description>
<url>https://github.com/austinv11/Discord4J</url>
<licenses>
Expand Down
10 changes: 6 additions & 4 deletions src/main/java/sx/blah/discord/api/internal/DiscordVoiceWS.java
Expand Up @@ -19,6 +19,7 @@
import sx.blah.discord.json.requests.VoiceSpeakingRequest;
import sx.blah.discord.json.requests.VoiceUDPConnectRequest;
import sx.blah.discord.json.responses.VoiceUpdateResponse;
import sx.blah.discord.util.AudioChannel;

import javax.net.ssl.SSLContext;
import java.io.BufferedReader;
Expand All @@ -41,7 +42,8 @@ public class DiscordVoiceWS extends WebSocketClient {
public static final int OPUS_SAMPLE_RATE = 48000; //(Hz) We want to use the highest of qualities! All the bandwidth!
public static final int OPUS_FRAME_SIZE = 960;
public static final int OPUS_FRAME_TIME_AMOUNT = OPUS_FRAME_SIZE*1000/OPUS_SAMPLE_RATE;
public static final int OPUS_CHANNEL_COUNT = 2; //Stereo audio channel
public static final int OPUS_MONO_CHANNEL_COUNT = 1;
public static final int OPUS_STEREO_CHANNEL_COUNT = 2;

public static final int OP_INITIAL_CONNECTION = 2;
public static final int OP_HEARTBEAT_RETURN = 3;
Expand Down Expand Up @@ -174,10 +176,10 @@ private void setupSendThread() {
public void run() {
try {
if (isConnected.get()) {
byte[] rawAudio = client.audioChannel.getAudioData(OPUS_FRAME_SIZE);
if (rawAudio != null) {
AudioChannel.AudioData data = client.audioChannel.getAudioData(OPUS_FRAME_SIZE);
if (data != null) {
client.timer = System.currentTimeMillis();
AudioPacket packet = new AudioPacket(seq, timestamp, ssrc, rawAudio, secret);
AudioPacket packet = new AudioPacket(seq, timestamp, ssrc, data.rawData, data.metaData.channels, secret);
if (!isSpeaking)
setSpeaking(true);
udpSocket.send(packet.asUdpPacket(addressPort));
Expand Down
40 changes: 27 additions & 13 deletions src/main/java/sx/blah/discord/api/internal/audio/AudioPacket.java
Expand Up @@ -33,20 +33,30 @@
*/
public class AudioPacket {

private static PointerByReference opusEncoder;
private static PointerByReference opusDecoder;
private static PointerByReference stereoOpusEncoder;
private static PointerByReference monoOpusEncoder;
private static PointerByReference stereoOpusDecoder;
private static PointerByReference monoOpusDecoder;

static {
try {
IntBuffer error = IntBuffer.allocate(4);
opusEncoder = Opus.INSTANCE.opus_encoder_create(DiscordVoiceWS.OPUS_SAMPLE_RATE, DiscordVoiceWS.OPUS_CHANNEL_COUNT, Opus.OPUS_APPLICATION_AUDIO, error);
stereoOpusEncoder = Opus.INSTANCE.opus_encoder_create(DiscordVoiceWS.OPUS_SAMPLE_RATE, DiscordVoiceWS.OPUS_STEREO_CHANNEL_COUNT, Opus.OPUS_APPLICATION_AUDIO, error);

error = IntBuffer.allocate(4);
opusDecoder = Opus.INSTANCE.opus_decoder_create(DiscordVoiceWS.OPUS_SAMPLE_RATE, DiscordVoiceWS.OPUS_CHANNEL_COUNT, error);
monoOpusEncoder = Opus.INSTANCE.opus_encoder_create(DiscordVoiceWS.OPUS_SAMPLE_RATE, DiscordVoiceWS.OPUS_MONO_CHANNEL_COUNT, Opus.OPUS_APPLICATION_AUDIO, error);

error = IntBuffer.allocate(4);
stereoOpusDecoder = Opus.INSTANCE.opus_decoder_create(DiscordVoiceWS.OPUS_SAMPLE_RATE, DiscordVoiceWS.OPUS_STEREO_CHANNEL_COUNT, error);

error = IntBuffer.allocate(4);
monoOpusDecoder = Opus.INSTANCE.opus_decoder_create(DiscordVoiceWS.OPUS_SAMPLE_RATE, DiscordVoiceWS.OPUS_MONO_CHANNEL_COUNT, error);
} catch (UnsatisfiedLinkError e) {
e.printStackTrace();
opusEncoder = null;
opusDecoder = null;
stereoOpusEncoder = null;
stereoOpusDecoder = null;
monoOpusEncoder = null;
monoOpusDecoder = null;
}
}

Expand All @@ -61,7 +71,7 @@ public AudioPacket(DatagramPacket packet) {
this(Arrays.copyOf(packet.getData(), packet.getLength()));
}

public AudioPacket(byte[] rawPacket) {
public AudioPacket(byte[] rawPacket) { //FIXME: Support mono & decryption
this.rawPacket = rawPacket;

ByteBuffer buffer = ByteBuffer.wrap(rawPacket);
Expand All @@ -75,20 +85,19 @@ public AudioPacket(byte[] rawPacket) {
this.rawAudio = decodeToPCM(encodedAudio);
}

public AudioPacket(char seq, int timestamp, int ssrc, byte[] rawAudio, byte[] secret) {
public AudioPacket(char seq, int timestamp, int ssrc, byte[] rawAudio, int channels, byte[] secret) {
this.seq = seq;
this.ssrc = ssrc;
this.timestamp = timestamp;
this.rawAudio = rawAudio;


ByteBuffer nonceBuffer = ByteBuffer.allocate(12);
nonceBuffer.put(0, (byte) 0x80);
nonceBuffer.put(1, (byte) 0x78);
nonceBuffer.putChar(2, seq);
nonceBuffer.putInt(4, timestamp);
nonceBuffer.putInt(8, ssrc);
this.encodedAudio = TweetNaCl.secretbox(AudioPacket.encodeToOpus(rawAudio),
this.encodedAudio = TweetNaCl.secretbox(encodeToOpus(rawAudio, channels),
Arrays.copyOf(nonceBuffer.array(), 24), //encryption nonce is 24 bytes long while discord's is 12 bytes long
secret);

Expand All @@ -107,7 +116,7 @@ public DatagramPacket asUdpPacket(InetSocketAddress address) {
return new DatagramPacket(getRawPacket(), rawPacket.length, address);
}

public static byte[] encodeToOpus(byte[] rawAudio) {
public static byte[] encodeToOpus(byte[] rawAudio, int channels) {
ShortBuffer nonEncodedBuffer = ShortBuffer.allocate(rawAudio.length/2);
ByteBuffer encoded = ByteBuffer.allocate(4096);
for (int i = 0; i < rawAudio.length; i += 2) {
Expand All @@ -122,7 +131,12 @@ public static byte[] encodeToOpus(byte[] rawAudio) {
nonEncodedBuffer.flip();

//TODO: check for 0 / negative value for error.
int result = Opus.INSTANCE.opus_encode(opusEncoder, nonEncodedBuffer, DiscordVoiceWS.OPUS_FRAME_SIZE, encoded, encoded.capacity());
int result;
if (channels == 1) {
result = Opus.INSTANCE.opus_encode(monoOpusEncoder, nonEncodedBuffer, DiscordVoiceWS.OPUS_FRAME_SIZE, encoded, encoded.capacity());
} else {
result = Opus.INSTANCE.opus_encode(stereoOpusEncoder, nonEncodedBuffer, DiscordVoiceWS.OPUS_FRAME_SIZE, encoded, encoded.capacity());
}

byte[] audio = new byte[result];
encoded.get(audio);
Expand All @@ -134,7 +148,7 @@ public byte[] decodeToPCM(byte[] opusAudio) {

ShortBuffer shortBuffer = nonEncodedBuffer.asShortBuffer();

int result = Opus.INSTANCE.opus_decode(opusDecoder, opusAudio, opusAudio.length, shortBuffer, shortBuffer.capacity(), 0);
int result = Opus.INSTANCE.opus_decode(stereoOpusDecoder, opusAudio, opusAudio.length, shortBuffer, shortBuffer.capacity(), 0);

nonEncodedBuffer.flip();

Expand Down
66 changes: 52 additions & 14 deletions src/main/java/sx/blah/discord/util/AudioChannel.java
Expand Up @@ -120,9 +120,10 @@ public void queueUrl(String url) {
*/
public void queueUrl(URL url) {
try {
metaDataQueue.add(new AudioMetaData(null, url, AudioSystem.getAudioFileFormat(url)));
BufferedInputStream bis = new BufferedInputStream(url.openStream());
queue(AudioSystem.getAudioInputStream(bis));
AudioInputStream stream = AudioSystem.getAudioInputStream(bis);
metaDataQueue.add(new AudioMetaData(null, url, AudioSystem.getAudioFileFormat(url), stream.getFormat().getChannels()));
queue(stream);
} catch (IOException | UnsupportedAudioFileException e) {
Discord4J.LOGGER.error("Discord Internal Exception", e);
}
Expand All @@ -144,8 +145,9 @@ public void queueFile(String file) {
*/
public void queueFile(File file) {
try {
metaDataQueue.add(new AudioMetaData(file, null, AudioSystem.getAudioFileFormat(file)));
queue(AudioSystem.getAudioInputStream(file));
AudioInputStream stream = AudioSystem.getAudioInputStream(file);
metaDataQueue.add(new AudioMetaData(file, null, AudioSystem.getAudioFileFormat(file), stream.getFormat().getChannels()));
queue(stream);
} catch (UnsupportedAudioFileException | IOException e) {
Discord4J.LOGGER.error("Discord Internal Exception", e);
}
Expand Down Expand Up @@ -188,7 +190,7 @@ public void queue(AudioInputStream inSource) {

if (metaDataQueue.size() == audioQueue.size()) { //Meta data wasn't added, user directly queued an audio inputstream
try {
metaDataQueue.add(new AudioMetaData(null, null, AudioSystem.getAudioFileFormat(inSource)));
metaDataQueue.add(new AudioMetaData(null, null, AudioSystem.getAudioFileFormat(inSource), audioFormat.getChannels()));
return;
} catch (UnsupportedAudioFileException | IOException e) {
Discord4J.LOGGER.error("Discord Internal Exception", e);
Expand All @@ -210,7 +212,7 @@ public void queue(AudioInputStream inSource) {
* @param length : How many MS of data needed to be sent.
* @return : The PCM data
*/
public byte[] getAudioData(int length) {
public AudioData getAudioData(int length) {
if (isPaused)
return null;

Expand All @@ -231,7 +233,7 @@ public byte[] getAudioData(int length) {
client.getDispatcher().dispatch(new AudioPlayEvent(data, metaData.fileSource,
metaData.urlSource, metaData.format));
}
return audio;
return new AudioData(audio, metaData);
} else {
audioQueue.remove(0);
metaDataQueue.remove(0);
Expand All @@ -250,16 +252,52 @@ public byte[] getAudioData(int length) {
/**
* Provides a small amount of information regarding the audio being played.
*/
private class AudioMetaData {
protected final File fileSource;
protected final URL urlSource;
protected final AudioFileFormat format;
protected volatile boolean startedReading = false;

public AudioMetaData(File fileSource, URL urlSource, AudioFileFormat format) {
public class AudioMetaData {
/**
* The file source (if present).
*/
public final File fileSource;
/**
* The url source (if present).
*/
public final URL urlSource;
/**
* The file format.
*/
public final AudioFileFormat format;
/**
* Whether the audio has been started reading.
*/
public volatile boolean startedReading = false;
/**
* The amount of channels in the audio.
*/
public final int channels;

public AudioMetaData(File fileSource, URL urlSource, AudioFileFormat format, int channels) {
this.fileSource = fileSource;
this.urlSource = urlSource;
this.format = format;
this.channels = channels;
}
}

/**
* Provides the raw audio data and other things including metadata and channel count.
*/
public static class AudioData {
/**
* The raw audio data.
*/
public final byte[] rawData;
/**
* The metadata for the audio.
*/
public final AudioMetaData metaData;

public AudioData(byte[] rawData, AudioMetaData metaData) {
this.rawData = rawData;
this.metaData = metaData;
}
}
}
3 changes: 2 additions & 1 deletion src/test/java/sx/blah/discord/TestBot.java
Expand Up @@ -204,7 +204,8 @@ public void handle(MessageReceivedEvent messageReceivedEvent) {
IVoiceChannel channel = client.getVoiceChannels().stream().filter(voiceChannel-> voiceChannel.getName().equalsIgnoreCase("Annoying Shit")).findFirst().orElse(null);
if (channel != null) {
channel.join();
client.getAudioChannel().queueFile(new File("./test.mp3"));
client.getAudioChannel().queueFile(new File("./test.mp3")); //Mono test
client.getAudioChannel().queueFile(new File("./test2.mp3")); //Stereo test
}
} else if (m.getContent().startsWith(".pause")) {
client.getAudioChannel().pause();
Expand Down
Binary file modified test.mp3
Binary file not shown.
Binary file added test2.mp3
Binary file not shown.

0 comments on commit 25cb0c2

Please sign in to comment.