Skip to content
Merged
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
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ const encoder = new OpusEncoder(48000, 2);
const encoded = encoder.encode(pcm);
const decoded = encoder.decode(encoded);
const decodedFloat = encoder.decodeFloat(encoded);

// 20 ms of interleaved stereo float32 PCM at 48 kHz.
const floatPcm = Buffer.from(new Float32Array(960 * 2).buffer);
const encodedFloat = encoder.encodeFloat(floatPcm);
```

---
Expand Down Expand Up @@ -109,6 +113,44 @@ const packet = encoder.encode(pcm);

---

### `encoder.encodeFloat(pcm: Buffer): Buffer`

Encode a single frame of 32-bit floating-point PCM into an Opus packet.

#### Input (encodeFloat)

- `pcm` – Node `Buffer` containing finite IEEE 754 **float32 PCM** samples:

- Interleaved by channel (LRLRLR… for stereo).
- Native little-endian byte order on the supported platforms.
- Exactly four bytes per sample and a total byte length divisible by
`channels * 4`.

- Each call must contain exactly one valid Opus frame: **2.5, 5, 10, 20, 40,
or 60 ms** per channel. The buffer must not be empty.

The method safely accepts Buffer slices with unaligned byte offsets; it copies
the PCM into aligned native storage before calling libopus.

#### Output (encodeFloat)

- Returns a `Buffer` containing a single Opus packet.
- The buffer length is the exact size of the encoded packet.

For example, encode 20 ms of mono float PCM at 16 kHz:

```js
const samplesPerFrame = 320;
const pcm = Buffer.alloc(samplesPerFrame * Float32Array.BYTES_PER_ELEMENT);
for (let index = 0; index < samplesPerFrame; index++) {
pcm.writeFloatLE(Math.sin((2 * Math.PI * index) / 40) * 0.5, index * 4);
}

const packet = encoder.encodeFloat(pcm);
```

---

### `encoder.decode(packet: Buffer): Buffer`

Decode a single Opus packet into PCM.
Expand Down
5 changes: 5 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ export interface OpusPlcDecodeOptions {

export interface OpusEncoder {
encode(buf: Buffer): Buffer;
/**
* Encodes interleaved, finite 32-bit floating-point little-endian PCM.
* The frame must be 2.5, 5, 10, 20, 40, or 60 ms long.
*/
encodeFloat(buf: Buffer): Buffer;
/**
* Decodes the given Opus buffer to PCM signed 16-bit little-endian
* @param buf Opus buffer
Expand Down
97 changes: 97 additions & 0 deletions src/node-opus.cc
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include <napi.h>
#include <cmath>
#include <cstring>
#include <vector>
#include "../libopus/opus/include/opus.h"

// -----------------------------------------------------------------------------
Expand All @@ -13,6 +14,7 @@
static constexpr int MAX_FRAME_SIZE = 5760; // 120 ms @ 48 kHz mono
static constexpr int MAX_PACKET_SIZE = 1276; // per Opus spec
static constexpr int MAX_FRAME_DURATION_MS = 120;
static constexpr int MAX_ENCODE_FRAME_DURATION_MS = 60;

static int MaxFrameSizeForRate(opus_int32 rate)
{
Expand All @@ -24,6 +26,26 @@ static int SamplesPer2Point5ms(opus_int32 rate)
return rate / 400;
}

static bool IsValidEncodeFrameSize(opus_int32 rate, int frameSize)
{
const int samplesPer2Point5ms = SamplesPer2Point5ms(rate);
if (frameSize < samplesPer2Point5ms || frameSize % samplesPer2Point5ms != 0)
return false;

switch (frameSize / samplesPer2Point5ms)
{
case 1: // 2.5 ms
case 2: // 5 ms
case 4: // 10 ms
case 8: // 20 ms
case 16: // 40 ms
case 24: // 60 ms
return true;
default:
return false;
}
}

// -----------------------------------------------------------------------------
// Utility: translate libopus error codes to strings
// -----------------------------------------------------------------------------
Expand Down Expand Up @@ -65,6 +87,7 @@ class OpusEncoderWrap : public Napi::ObjectWrap<OpusEncoderWrap>
private:
// JS‑exposed methods
Napi::Value Encode(const Napi::CallbackInfo &);
Napi::Value EncodeFloat(const Napi::CallbackInfo &);
Napi::Value Decode(const Napi::CallbackInfo &);
Napi::Value DecodeFloat(const Napi::CallbackInfo &);
Napi::Value ApplyEncoderCTL(const Napi::CallbackInfo &);
Expand Down Expand Up @@ -185,6 +208,79 @@ Napi::Value OpusEncoderWrap::Encode(const Napi::CallbackInfo &info)
return Napi::Buffer<char>::Copy(env, reinterpret_cast<char *>(outOpus_), clen);
}

// -----------------------------------------------------------------------------
// Encode 32-bit float PCM -> Opus packet (returns Buffer)
// -----------------------------------------------------------------------------
Napi::Value OpusEncoderWrap::EncodeFloat(const Napi::CallbackInfo &info)
{
Napi::Env env = info.Env();
if (info.Length() < 1 || !info[0].IsBuffer())
{
Napi::TypeError::New(env, "Argument must be a Buffer containing 32-bit float PCM").ThrowAsJavaScriptException();
return env.Null();
}

if (EnsureEncoder() != OPUS_OK)
{
Napi::Error::New(env, "Failed to create libopus encoder (bad params?)").ThrowAsJavaScriptException();
return env.Null();
}

Napi::Buffer<unsigned char> buf = info[0].As<Napi::Buffer<unsigned char>>();
const size_t bytesPerFrame = sizeof(float) * static_cast<size_t>(channels_);
if (buf.Length() == 0)
{
Napi::RangeError::New(env, "Float PCM buffer must contain at least one frame").ThrowAsJavaScriptException();
return env.Null();
}
if (buf.Length() % bytesPerFrame != 0)
{
Napi::RangeError::New(env, "Float PCM buffer length must be a multiple of (channels*4 bytes)").ThrowAsJavaScriptException();
return env.Null();
}

const size_t frameSize = buf.Length() / bytesPerFrame;
const size_t maxEncodeFrameSize = static_cast<size_t>(rate_) * MAX_ENCODE_FRAME_DURATION_MS / 1000;
if (frameSize > maxEncodeFrameSize)
{
Napi::RangeError::New(env, "Float PCM frame exceeds the maximum 60 ms Opus encode duration").ThrowAsJavaScriptException();
return env.Null();
}
if (!IsValidEncodeFrameSize(rate_, static_cast<int>(frameSize)))
{
Napi::RangeError::New(env, "Float PCM frame size must be one of 2.5, 5, 10, 20, 40, or 60 ms").ThrowAsJavaScriptException();
return env.Null();
}

// Buffer data is byte-aligned, not necessarily float-aligned (for example,
// Buffer#subarray(1)). Copy it before reading floats to avoid undefined
// behavior on architectures that require aligned float access.
std::vector<float> pcm(frameSize * static_cast<size_t>(channels_));
std::memcpy(pcm.data(), buf.Data(), buf.Length());
for (float sample : pcm)
{
if (!std::isfinite(sample))
{
Napi::RangeError::New(env, "Float PCM samples must be finite").ThrowAsJavaScriptException();
return env.Null();
}
}

int clen = opus_encode_float(enc_, pcm.data(), static_cast<int>(frameSize), outOpus_, MAX_PACKET_SIZE);
if (clen < 0)
{
Napi::Error::New(env, StrError(clen)).ThrowAsJavaScriptException();
return env.Null();
}
if (clen == 0)
{
Napi::Error::New(env, "libopus encoder produced an empty packet").ThrowAsJavaScriptException();
return env.Null();
}

return Napi::Buffer<char>::Copy(env, reinterpret_cast<char *>(outOpus_), clen);
}

// -----------------------------------------------------------------------------
// Decode Opus packet -> PCM buffer
// -----------------------------------------------------------------------------
Expand Down Expand Up @@ -436,6 +532,7 @@ Napi::Object OpusEncoderWrap::Init(Napi::Env env, Napi::Object exports)
{
Napi::Function ctor = Napi::ObjectWrap<OpusEncoderWrap>::DefineClass(env, "OpusEncoder", {
InstanceMethod("encode", &OpusEncoderWrap::Encode),
InstanceMethod("encodeFloat", &OpusEncoderWrap::EncodeFloat),
InstanceMethod("decode", &OpusEncoderWrap::Decode),
InstanceMethod("decodeFloat", &OpusEncoderWrap::DecodeFloat),
InstanceMethod("applyEncoderCTL", &OpusEncoderWrap::ApplyEncoderCTL),
Expand Down
1 change: 1 addition & 0 deletions src/node-opus.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class OpusEncoderWrap : public Napi::ObjectWrap<OpusEncoderWrap>

// JS-exposed methods
Napi::Value Encode(const Napi::CallbackInfo &info);
Napi::Value EncodeFloat(const Napi::CallbackInfo &info);
Napi::Value Decode(const Napi::CallbackInfo &info);
Napi::Value DecodeFloat(const Napi::CallbackInfo &info);
Napi::Value ApplyEncoderCTL(const Napi::CallbackInfo &info);
Expand Down
Loading
Loading