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
68 changes: 68 additions & 0 deletions src/node-opus.cc
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Object NodeOpusEncoder::Init(Napi::Env env, Object exports) {
Function func = DefineClass(env, "OpusEncoder", {
InstanceMethod("encode", &NodeOpusEncoder::Encode),
InstanceMethod("decode", &NodeOpusEncoder::Decode),
InstanceMethod("conceal", &NodeOpusEncoder::Conceal),
InstanceMethod("applyEncoderCTL", &NodeOpusEncoder::ApplyEncoderCTL),
InstanceMethod("applyDecoderCTL", &NodeOpusEncoder::ApplyDecoderCTL),
InstanceMethod("setBitrate", &NodeOpusEncoder::SetBitrate),
Expand Down Expand Up @@ -177,6 +178,73 @@ Napi::Value NodeOpusEncoder::Decode(const CallbackInfo& args) {
return env.Null();
}

Napi::Value NodeOpusEncoder::Conceal(const CallbackInfo& args) {
Napi::Env env = args.Env();

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

if (!args[0].IsNumber()) {
Napi::TypeError::New(env, "frame_size parameter must be a number").ThrowAsJavaScriptException();
return env.Null();
}

int frame_size = args[0].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();
}

// Optional packet parameter for FEC
unsigned char* compressedData = nullptr;
size_t compressedDataLength = 0;
int decode_fec = 0;

if (args.Length() >= 2 && !args[1].IsNull() && !args[1].IsUndefined()) {
if (!args[1].IsBuffer()) {
Napi::TypeError::New(env, "Packet parameter must be a buffer, null, or undefined").ThrowAsJavaScriptException();
return env.Null();
}

Buffer<unsigned char> buf = args[1].As<Buffer<unsigned char>>();
compressedData = buf.Data();
compressedDataLength = buf.Length();
decode_fec = 1; // Enable FEC when packet is provided
}

if (this->EnsureDecoder() != OPUS_OK) {
Napi::Error::New(env, "Could not create decoder. Check the decoder parameters").ThrowAsJavaScriptException();
return env.Null();
}

int decodedSamples = opus_decode(
this->decoder,
compressedData,
compressedDataLength,
&(this->outPcm[0]),
frame_size,
decode_fec
);

if (decodedSamples < 0) {
Napi::TypeError::New(env, getDecodeError(decodedSamples)).ThrowAsJavaScriptException();
return env.Null();
}

int decodedLength = decodedSamples * 2 * this->channels;

Buffer<char> actualBuf = Buffer<char>::Copy(env, reinterpret_cast<char*>(this->outPcm), decodedLength);

if (!actualBuf.IsEmpty()) return actualBuf;

Napi::Error::New(env, "Could not decode the data").ThrowAsJavaScriptException();
return env.Null();
}

void NodeOpusEncoder::ApplyEncoderCTL(const CallbackInfo& args) {
Napi::Env env = args.Env();

Expand Down
4 changes: 3 additions & 1 deletion src/node-opus.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ class NodeOpusEncoder : public ObjectWrap<NodeOpusEncoder> {
Napi::Value Encode(const CallbackInfo& args);

Napi::Value Decode(const CallbackInfo& args);


Napi::Value Conceal(const CallbackInfo& args);

void ApplyEncoderCTL(const CallbackInfo& args);

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

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

// Conceal with specific frame_size for proper PLC
// For 16kHz, 20ms = 320 samples, so we expect 320 * 2 bytes = 640 bytes output
const plcFrame = opus.conceal(320);
assert(plcFrame.length === 640, `PLC frame with frame_size=320 should be 640 bytes, got ${plcFrame.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.conceal(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) with conceal method
{
const opus = new OpusEncoder(16_000, 1);
const frame = fs.readFileSync(path.join(__dirname, 'frame.opus'));

// Conceal with FEC using a packet (frame_size=320 for 20ms at 16kHz)
const decoded1 = opus.conceal(320, frame);
assert(decoded1.length === 640, 'FEC decoded frame length is not 640');

// Conceal without packet (PLC only)
const decoded2 = opus.conceal(320);
assert(decoded2.length === 640, 'PLC decoded frame length is not 640');

// Conceal with null packet (PLC)
const decoded3 = opus.conceal(320, null);
assert(decoded3.length === 640, 'PLC decoded frame length is not 640');
}

console.log('Passed');
6 changes: 6 additions & 0 deletions typings/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ declare module '@discordjs/opus' {
* @param buf Opus buffer
*/
public decode(buf: Buffer): Buffer;
/**
* Performs packet loss concealment (PLC) or forward error correction (FEC)
* @param frame_size Number of samples per channel to generate. This must be exactly the duration of the missing audio (e.g., 960 for 20ms at 48kHz, 320 for 20ms at 16kHz).
* @param packet Optional Opus packet buffer for FEC. If provided, FEC will be used to reconstruct the audio. If omitted, null, or undefined, PLC will generate synthetic audio.
*/
public conceal(frame_size: number, packet?: Buffer | null): Buffer;
public applyEncoderCTL(ctl: number, value: number): void;
public applyDecoderCTL(ctl: number, value: number): void;
public setBitrate(bitrate: number): void;
Expand Down