Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 55 additions & 9 deletions src/node-opus.cc
Original file line number Diff line number Diff line change
Expand Up @@ -135,18 +135,64 @@ Napi::Value NodeOpusEncoder::Decode(const CallbackInfo& args) {
Napi::Env env = args.Env();

if (args.Length() < 1) {
Napi::RangeError::New(env, "Expected 1 argument").ThrowAsJavaScriptException();
Napi::RangeError::New(env, "Expected at least 1 argument").ThrowAsJavaScriptException();
return env.Null();
}

if (!args[0].IsBuffer()) {
Napi::TypeError::New(env, "Provided input needs to be a buffer").ThrowAsJavaScriptException();
return env.Null();
// Support null/undefined for packet loss concealment (PLC)
unsigned char* compressedData = nullptr;
size_t compressedDataLength = 0;

if (!args[0].IsNull() && !args[0].IsUndefined()) {
if (!args[0].IsBuffer()) {
Napi::TypeError::New(env, "Provided input needs to be a buffer, null, or undefined").ThrowAsJavaScriptException();
return env.Null();
}

Buffer<unsigned char> buf = args[0].As<Buffer<unsigned char>>();
compressedData = buf.Data();
compressedDataLength = buf.Length();
}

// Optional decode_fec parameter (defaults to 0)
int decode_fec = 0;
if (args.Length() >= 2 && !args[1].IsUndefined()) {
if (!args[1].IsNumber() && !args[1].IsBoolean()) {
Napi::TypeError::New(env, "decode_fec parameter must be a number or boolean").ThrowAsJavaScriptException();
return env.Null();
}

if (args[1].IsBoolean()) {
decode_fec = args[1].ToBoolean().Value() ? 1 : 0;
} else {
decode_fec = args[1].ToNumber().Int32Value();
}
}

Buffer<unsigned char> buf = args[0].As<Buffer<unsigned char>>();
unsigned char* compressedData = buf.Data();
size_t compressedDataLength = buf.Length();
// Optional frame_size parameter (defaults to inferred or MAX_FRAME_SIZE)
// For PLC (data==NULL) or FEC (decode_fec=1), frame_size should be exactly
// the duration of the missing audio, otherwise the decoder will not be in
// the optimal state to decode the next incoming packet.
int frame_size = MAX_FRAME_SIZE;
if (args.Length() >= 3 && !args[2].IsUndefined()) {
if (!args[2].IsNumber()) {
Napi::TypeError::New(env, "frame_size parameter must be a number").ThrowAsJavaScriptException();
return env.Null();
}
frame_size = args[2].ToNumber().Int32Value();

// Validate frame_size
if (frame_size <= 0 || frame_size > MAX_FRAME_SIZE) {
Napi::RangeError::New(env, "frame_size must be between 1 and " + std::to_string(MAX_FRAME_SIZE)).ThrowAsJavaScriptException();
return env.Null();
}
} else if (compressedData != nullptr) {
// Try to infer frame_size from the packet
int inferred_size = opus_packet_get_nb_samples(compressedData, compressedDataLength, this->rate);
if (inferred_size > 0 && inferred_size <= MAX_FRAME_SIZE) {
frame_size = inferred_size;
}
}

if (this->EnsureDecoder() != OPUS_OK) {
Napi::Error::New(env, "Could not create decoder. Check the decoder parameters").ThrowAsJavaScriptException();
Expand All @@ -158,8 +204,8 @@ Napi::Value NodeOpusEncoder::Decode(const CallbackInfo& args) {
compressedData,
compressedDataLength,
&(this->outPcm[0]),
MAX_FRAME_SIZE,
/* decode_fec */ 0
frame_size,
decode_fec
);

if (decodedSamples < 0) {
Expand Down
45 changes: 45 additions & 0 deletions tests/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,49 @@ const { OpusEncoder } = require('../lib/index.js');
assert.throws(() => new OpusEncoder(16000, null), /Expected channels to be a number/);
}

// Packet loss concealment (PLC) with null/undefined
{
const opus = new OpusEncoder(16_000, 1);

// Decode with null for packet loss concealment (using default MAX_FRAME_SIZE)
const plcFrame = opus.decode(null);
assert(plcFrame.length > 0, 'PLC frame should have length > 0');

// Decode with undefined for packet loss concealment (using default MAX_FRAME_SIZE)
const plcFrame2 = opus.decode(undefined);
assert(plcFrame2.length > 0, 'PLC frame should have length > 0');

// Decode with null and specific frame_size for proper PLC
// For 16kHz, 20ms = 320 samples, so we expect 320 * 2 bytes = 640 bytes output
const plcFrame3 = opus.decode(null, 0, 320);
assert(plcFrame3.length === 640, `PLC frame with frame_size=320 should be 640 bytes, got ${plcFrame3.length}`);

// Test with 48kHz decoder
const opus48 = new OpusEncoder(48_000, 2);
// For 48kHz stereo, 20ms = 960 samples per channel, output is 960 * 2 channels * 2 bytes = 3840 bytes
const plcFrame48 = opus48.decode(null, 0, 960);
assert(plcFrame48.length === 3840, `PLC frame for 48kHz stereo with frame_size=960 should be 3840 bytes, got ${plcFrame48.length}`);
}

// Forward error correction (FEC) parameter
{
const opus = new OpusEncoder(16_000, 1);
const frame = fs.readFileSync(path.join(__dirname, 'frame.opus'));

// Decode with decode_fec = 0 (default)
const decoded1 = opus.decode(frame, 0);
assert(decoded1.length === 640, 'Decoded frame length is not 640');

// Decode with decode_fec = 1
const decoded2 = opus.decode(frame, 1);
assert(decoded2.length === 640, 'Decoded frame length is not 640');

// Decode with decode_fec as boolean
const decoded3 = opus.decode(frame, false);
assert(decoded3.length === 640, 'Decoded frame length is not 640');

const decoded4 = opus.decode(frame, true);
assert(decoded4.length === 640, 'Decoded frame length is not 640');
}

console.log('Passed');
6 changes: 4 additions & 2 deletions typings/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ declare module '@discordjs/opus' {
public encode(buf: Buffer): Buffer;
/**
* Decodes the given Opus buffer to PCM signed 16-bit little-endian
* @param buf Opus buffer
* @param buf Opus buffer, or null/undefined for packet loss concealment (PLC)
* @param decode_fec Optional flag to enable forward error correction (0 or 1, default 0)
* @param frame_size Optional number of samples per channel to decode. For PLC or FEC, this must be exactly the duration of the missing audio (e.g., 960 for 20ms at 48kHz). Defaults to maximum frame size for normal decoding.
*/
public decode(buf: Buffer): Buffer;
public decode(buf: Buffer | null | undefined, decode_fec?: number | boolean, frame_size?: number): Buffer;
public applyEncoderCTL(ctl: number, value: number): void;
public applyDecoderCTL(ctl: number, value: number): void;
public setBitrate(bitrate: number): void;
Expand Down