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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ const encoder = new OpusEncoder(48_000, 2);
// Encode and decode.
const encoded = encoder.encode(buffer);
const decoded = encoder.decode(encoded);

// Encode and decode in float32.
const encodedFP = encoder.encodeFloat(buffer);
const decodedFP = encoder.decodeFloat(encodedFP);
```

## Platform support
Expand Down
85 changes: 85 additions & 0 deletions src/node-opus.cc
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ Object NodeOpusEncoder::Init(Napi::Env env, Object exports) {

Function func = DefineClass(env, "OpusEncoder", {
InstanceMethod("encode", &NodeOpusEncoder::Encode),
InstanceMethod("encodeFloat", &NodeOpusEncoder::EncodeFloat),
InstanceMethod("decode", &NodeOpusEncoder::Decode),
InstanceMethod("decodeFloat", &NodeOpusEncoder::DecodeFloat),
InstanceMethod("applyEncoderCTL", &NodeOpusEncoder::ApplyEncoderCTL),
InstanceMethod("applyDecoderCTL", &NodeOpusEncoder::ApplyDecoderCTL),
InstanceMethod("setBitrate", &NodeOpusEncoder::SetBitrate),
Expand All @@ -45,6 +47,7 @@ NodeOpusEncoder::NodeOpusEncoder(const CallbackInfo& args): ObjectWrap<NodeOpusE
this->encoder = nullptr;
this->decoder = nullptr;
this->outPcm = nullptr;
this->outFloat = nullptr;

this->rate = 48000;
this->channels = 2;
Expand All @@ -67,6 +70,7 @@ NodeOpusEncoder::NodeOpusEncoder(const CallbackInfo& args): ObjectWrap<NodeOpusE

this->application = OPUS_APPLICATION_AUDIO;
this->outPcm = new opus_int16[this->channels * MAX_FRAME_SIZE];
this->outFloat = new float[this->channels * MAX_FRAME_SIZE];
}

NodeOpusEncoder::~NodeOpusEncoder() {
Expand All @@ -77,7 +81,9 @@ NodeOpusEncoder::~NodeOpusEncoder() {
this->decoder = nullptr;

if (this->outPcm) delete this->outPcm;
if (this->outFloat) delete this->outFloat;
Comment on lines 79 to +84
Copy link

Copilot AI Dec 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using delete on an array allocated with new[] is undefined behavior and can cause memory leaks or crashes. Since outFloat is allocated with new float[channels * MAX_FRAME_SIZE] on line 66, it should be deallocated with delete[] instead of delete. The same issue exists with outPcm on line 76.

Suggested change
if (this->outPcm) delete this->outPcm;
if (this->outFloat) delete this->outFloat;
if (this->outPcm) delete[] this->outPcm;
if (this->outFloat) delete[] this->outFloat;

Copilot uses AI. Check for mistakes.
this->outPcm = nullptr;
this->outFloat = nullptr;
}

int NodeOpusEncoder::EnsureEncoder() {
Expand Down Expand Up @@ -131,6 +137,39 @@ Napi::Value NodeOpusEncoder::Encode(const CallbackInfo& args) {
return env.Null();
}

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

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

if (args.Length() < 1) {
Napi::RangeError::New(env, "Expected 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();
}

Buffer<char> buf = args[0].As<Buffer<char>>();
char* pcmData = buf.Data();
float* pcm = reinterpret_cast<float*>(pcmData);
int frameSize = buf.Length() / 4 / this->channels;

int compressedLength = opus_encode_float(this->encoder, pcm, frameSize, &(this->outOpus[0]), MAX_PACKET_SIZE);

Copy link

Copilot AI Dec 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing error handling for the return value of opus_encode_float. According to the Opus API documentation, opus_encode_float can return negative values on error (same error codes as opus_decode_float). This function should check if compressedLength < 0 and throw an appropriate error, similar to how DecodeFloat checks for negative return values on line 243. Without this check, negative error codes could be passed to Buffer::Copy causing undefined behavior.

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

Copilot uses AI. Check for mistakes.
Buffer<char> actualBuf = Buffer<char>::Copy(env, reinterpret_cast<char*>(this->outOpus), compressedLength);

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

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

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

Expand Down Expand Up @@ -177,6 +216,52 @@ Napi::Value NodeOpusEncoder::Decode(const CallbackInfo& args) {
return env.Null();
}

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

if (args.Length() < 1) {
Napi::RangeError::New(env, "Expected 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();
}

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

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_float(
this->decoder,
compressedData,
compressedDataLength,
&(this->outFloat[0]),
MAX_FRAME_SIZE,
/* decode_fec */ 0
);

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

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

Buffer<char> actualBuf = Buffer<char>::Copy(env, reinterpret_cast<char*>(this->outFloat), 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
5 changes: 5 additions & 0 deletions src/node-opus.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class NodeOpusEncoder : public ObjectWrap<NodeOpusEncoder> {

unsigned char outOpus[MAX_PACKET_SIZE];
opus_int16* outPcm;
float* outFloat;

protected:
int EnsureEncoder();
Expand All @@ -30,8 +31,12 @@ class NodeOpusEncoder : public ObjectWrap<NodeOpusEncoder> {
~NodeOpusEncoder();

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

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

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

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

void ApplyEncoderCTL(const CallbackInfo& args);

Expand Down
9 changes: 9 additions & 0 deletions tests/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@ const { OpusEncoder } = require('../lib/index.js');

assert(decoded.length === 640, 'Decoded frame length is not 640');
assert(reEncoded.length === 45, 're-encoded frame length is not 45');

const decodedFloat = opus.decodeFloat(frame);

const reEncodedFloat = opus.encodeFloat(decodedFloat);

// float32 buffer should be 2x the size of int16 buffer
assert(decodedFloat.length === 1280, 'Decoded float frame length is not 1280');
// Encoded size differs slightly due to precision differences
assert(reEncodedFloat.length === 43, 're-encoded float frame length is not 43');
}

// Default values work
Expand Down