Skip to content

Commit

Permalink
Merge pull request #30 from kripton/receiveAudio
Browse files Browse the repository at this point in the history
Implement receiving and decoding (opus) voice data packets
  • Loading branch information
Gielert authored Oct 25, 2020
2 parents 50f64ae + 8ea8e60 commit 58cf5bb
Show file tree
Hide file tree
Showing 3 changed files with 138 additions and 3 deletions.
4 changes: 4 additions & 0 deletions src/Client.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,10 @@ class Client extends EventEmitter {
this.connection.on('ChannelState', data => channelState.handle(data))
// this.connection.on('CryptSetup', data => console.log(data))
this.connection.on('TextMessage', data => textMessage.handle(data));

this.connection.on('voiceData', (voiceData) => {
this.emit('voiceData', voiceData)
})
}

/**
Expand Down
83 changes: 80 additions & 3 deletions src/Connection.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class Connection extends EventEmitter {
this.currentEncoder = this.opusEncoder
this.codec = Connection.codec().Opus
this.voiceSequence = 0

this.codecWarningShown = {};
}

connect() {
Expand All @@ -38,6 +38,9 @@ class Connection extends EventEmitter {
static codec() {
return {
Celt: 0,
Ping: 1,
Speex: 2,
CeltBeta: 3,
Opus: 4
}
}
Expand All @@ -55,8 +58,8 @@ class Connection extends EventEmitter {
}

_processData(type, data) {
if( this.protobuf.nameById(type) === 'UDPTunnel' ) {
//TODO handle voice packets
if (this.protobuf.nameById(type) === 'UDPTunnel' ) {
this.readAudio(data);
} else {
var msg = this.protobuf.decodePacket(type, data);
this._processMessage(type, msg);
Expand Down Expand Up @@ -90,6 +93,80 @@ class Connection extends EventEmitter {

}

readAudio(data) {
// Packet format:
// https://github.com/mumble-voip/mumble-protocol/blob/master/voice_data.rst#packet-format
const audioType = (data[0] & 0xE0) >> 5;
const audioTarget = data[0] & 0x1F;

//console.debug("\nAUDIO DATA length:" + data.length + ' audioType:' + audioType + ' audioTarget: ' + audioTarget);

if (audioType == Connection.codec().Ping) {
// Nothing to do but don't display a warning
console.log('Audio PING packet received');
return;
} else if (audioType > 4) {
// We don't know what type this is
console.warn('Unknown audioType in packet detected: ' + audioType);
return;
}

// It's an "Encoded audio data packet" (CELT Alpha, Speex, CELT Beta
// or Opus). So it's safe to parse the header

// Offset in data from where we are currently reading
var offset = 1;

var varInt = Util.fromVarInt(data.slice(offset, offset + 9));
const sender = varInt.value;
offset += varInt.consumed;

varInt = Util.fromVarInt(data.slice(offset, offset + 9));
const sequence = varInt.value;
offset += varInt.consumed;

if (audioType != Connection.codec().Opus) {
// Not OPUS-encoded => not supported :/
// Check if we already printed a warning for this audiostream
if ((!this.codecWarningShown[sender]) || (sequence < this.codecWarningShown[sender])) {
console.warn('Unspported audio codec in voice stream from user ' + sender + ': ', audioType);
}
this.codecWarningShown[sender] = sequence;
return;
}

//console.debug("\tsender:" + sender + ' sequence:' + sequence);

// Opus header
varInt = Util.fromVarInt(data.slice(offset, offset + 9));
offset += varInt.consumed;
const opusHeader = varInt.value;

const opusLength = opusHeader & 0x1FFF;
const lastFrame = (opusHeader & 0x2000) ? true : false;

//console.debug("\topus header:" + opusHeader + ' length:' + opusLength + ' lastFrame:' + lastFrame);

const opusData = data.slice(offset, offset + opusLength);

//console.debug("\tOPUS DATA LENGTH:" + opusData.length + ' DATA:', opusData);

const decoded = this.currentEncoder.decode(opusData);
//console.debug("\tDECODED DATA LENGTH:" + decoded.length + ' DATA:', decoded);

const voiceData = {
audioType: audioType, // For the moment, will be 4 = OPUS
whisperTarget: audioTarget,
sender: sender, // Session ID of the user sending the audio
sequence: sequence,
lastFrame: lastFrame, // Don't rely on it!
opusData: opusData, // Voice data encoded, as it came in
decodedData: decoded // Voice data decoded (48000, 1ch, 16bit)
}

this.emit('voiceData', voiceData);
}

writeAudio(packet, whisperTarget, codec, voiceSequence, final) {
packet = this.currentEncoder.encode(packet)

Expand Down
54 changes: 54 additions & 0 deletions src/Util.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,60 @@ class Util {
};
}

static fromVarInt(buf) {
// TODO: 111110__ + varint Negative recursive varint
// TODO: 111111xx Byte-inverted negative two bit number (~xx)

var retVal = {
value: 0,
consumed: 0
}

if (buf[0] < 0x80) {
// 0xxxxxxx 7 bit positive number
retVal.value = buf[0];
retVal.consumed = 1;
} else if (buf[0] < 0xC0) {
// 10xxxxxx + 1 byte 14-bit positive number
retVal.value = (buf[0] & 0x3F) << 8;
retVal.value |= buf[1];
retVal.consumed = 2;
} else if (buf[0] < 0xE0) {
// 110xxxxx + 2 bytes 21-bit positive number
retVal.value = (buf[0] & 0x1F) << 16;
retVal.value |= (buf[1]) << 8;
retVal.value |= (buf[2]);
retVal.consumed = 3;
} else if (buf[0] < 0xF0) {
// 1110xxxx + 3 bytes 28-bit positive number
retVal.value = (buf[0] & 0x0F) << 24;
retVal.value |= (buf[1]) << 16;
retVal.value |= (buf[2]) << 8;
retVal.value |= (buf[3]);
retVal.consumed = 4;
} else if (buf[0] < 0xF4) {
// 111100__ + int (32-bit)
retVal.value = (buf[1]) << 24;
retVal.value |= (buf[2]) << 16;
retVal.value |= (buf[3]) << 8;
retVal.value |= (buf[4]);
retVal.consumed = 5;
} else if (buf[0] < 0xFC) {
// 111101__ + long (64-bit)
retVal.value = (buf[1]) << 56;
retVal.value |= (buf[2]) << 48;
retVal.value |= (buf[3]) << 40;
retVal.value |= (buf[4]) << 32;
retVal.value |= (buf[5]) << 24;
retVal.value |= (buf[6]) << 16;
retVal.value |= (buf[7]) << 8;
retVal.value |= (buf[8]);
retVal.consumed = 9;
}

return retVal;
}

static encodeVersion(major, minor, patch) {
return ((major & 0xffff) << 16) |
((minor & 0xff) << 8) |
Expand Down

0 comments on commit 58cf5bb

Please sign in to comment.