diff --git a/src/node-opus.cc b/src/node-opus.cc index 930cb8b..2a1c82f 100644 --- a/src/node-opus.cc +++ b/src/node-opus.cc @@ -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 buf = args[0].As>(); + 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 buf = args[0].As>(); - 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(); @@ -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) { diff --git a/tests/test.js b/tests/test.js index b28b543..5026da7 100644 --- a/tests/test.js +++ b/tests/test.js @@ -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'); diff --git a/typings/index.d.ts b/typings/index.d.ts index eaa6757..6404cfe 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -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;