@imcooder/opuslib 0.2.1 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -29,28 +29,36 @@
29
29
  > });
30
30
  > ```
31
31
  >
32
- > **New Fields**
33
- > - **`audioLevel`** Each `audioChunk` event includes a normalized `audioLevel` (0.0~1.0), computed via configurable RMS sliding window (default 360ms) with dBFS-to-linear mapping (IEC 61606).
32
+ > **`packetDuration` now works (was ignored in original)**
33
+ > - In the original library, `packetDuration` was accepted but had no effect. Now it controls how many Opus frames are batched before emitting an `audioChunk` event, reducing JS bridge calls.
34
+ > - Example: `frameSize=20ms, packetDuration=100ms` → 5 frames encoded individually, batched into one `audioChunk` event (80% fewer bridge calls).
35
+ >
36
+ > **New `audioChunk` fields**
37
+ > - **`audioLevel`** — Normalized audio level (0.0~1.0), computed via configurable RMS sliding window (default 360ms) with dBFS-to-linear mapping (IEC 61606).
38
+ > - **`duration`** — Duration of this packet in milliseconds (`frameSize * frameCount`).
39
+ > - **`frameCount`** — Number of Opus frames contained in this packet.
40
+ > - **`preSkip`** — (in `audioStarted` event) Opus encoder lookahead in samples. Decoders should skip this many samples at the beginning of the stream.
34
41
  > ```typescript
35
42
  > Opuslib.addListener('audioChunk', (event) => {
36
- > // event.data: ArrayBuffer (Opus encoded packet)
43
+ > // event.data: ArrayBuffer (batched Opus encoded frames)
37
44
  > // event.timestamp: 1711000000100 (ms since epoch)
38
45
  > // event.sequenceNumber: 5 (packet counter)
39
46
  > // event.audioLevel: 0.72 (0=silence, 1=loud)
47
+ > // event.duration: 100 (ms, = frameSize * frameCount)
48
+ > // event.frameCount: 5 (number of Opus frames in this packet)
40
49
  > });
41
50
  > ```
42
- > - **`preSkip`** — Opus encoder lookahead in samples, returned in `audioStarted` event. Decoders should skip this many samples at the beginning of the stream.
43
51
  >
44
52
  > **New Config Options**
45
- > - **`audioLevelWindow`** — RMS window duration in milliseconds for audio level calculation (default: 360ms). Shorter window = more responsive, longer window = smoother.
53
+ > - **`audioLevelWindow`** — RMS window duration in milliseconds for audio level calculation (default: 360ms). Shorter = more responsive, longer = smoother.
46
54
  > ```typescript
47
55
  > await Opuslib.startStreaming({
48
56
  > sampleRate: 16000,
49
57
  > channels: 1,
50
58
  > bitrate: 24000,
51
59
  > frameSize: 20,
52
- > packetDuration: 20,
53
- > audioLevelWindow: 200, // 200ms window (default: 360ms)
60
+ > packetDuration: 100, // batch 5 frames per event (was ignored before)
61
+ > audioLevelWindow: 200, // 200ms RMS window (default: 360ms)
54
62
  > });
55
63
  > ```
56
64
 
@@ -269,8 +277,10 @@ Emitted when an encoded Opus packet is ready.
269
277
 
270
278
  ```typescript
271
279
  Opuslib.addListener('audioChunk', (event: AudioChunkEvent) => {
272
- // event.data: ArrayBuffer - Raw Opus packet (ready to send/save)
280
+ // event.data: ArrayBuffer - Batched Opus frames (ready to send/save)
273
281
  // event.audioLevel: number - Audio level 0.0~1.0 (0=silence, 1=loud)
282
+ // event.duration: number - Duration in ms (frameSize * frameCount)
283
+ // event.frameCount: number - Number of Opus frames in this packet
274
284
  });
275
285
  ```
276
286
 
@@ -278,10 +288,12 @@ Opuslib.addListener('audioChunk', (event: AudioChunkEvent) => {
278
288
 
279
289
  ```typescript
280
290
  interface AudioChunkEvent {
281
- data: ArrayBuffer; // Raw Opus-encoded audio packet
291
+ data: ArrayBuffer; // Batched Opus-encoded frames
282
292
  timestamp: number; // Milliseconds since epoch
283
293
  sequenceNumber: number; // Incrementing packet counter
284
294
  audioLevel: number; // Audio level 0.0~1.0 (360ms RMS window, 0=silence, 1=loud)
295
+ duration: number; // Packet duration in ms (frameSize * frameCount)
296
+ frameCount: number; // Number of Opus frames in this packet
285
297
  }
286
298
  ```
287
299
 
@@ -31,6 +31,9 @@ class AudioProcessor(private val config: AudioConfig) {
31
31
  private var opusEncoder: OpusEncoder? = null
32
32
  private val pendingSamples = mutableListOf<Short>()
33
33
  private val samplesPerFrame: Int = (config.sampleRate * config.frameSize / 1000.0).toInt()
34
+ private val framesPerPacket: Int = Math.max(1, (config.packetDuration / config.frameSize).toInt())
35
+ private var packetBuffer = java.io.ByteArrayOutputStream() // accumulates encoded frames
36
+ private var packetFrameCount: Int = 0
34
37
  private var sequenceNumber: Int = 0
35
38
  private var startTime: Double = 0.0
36
39
 
@@ -44,7 +47,8 @@ class AudioProcessor(private val config: AudioConfig) {
44
47
  private var pcmFileOutputStream: FileOutputStream? = null
45
48
 
46
49
  // Event callbacks (all invoked on encoding thread)
47
- private var onAudioChunk: ((ByteArray, Double, Int, Float) -> Unit)? = null
50
+ // onAudioChunk: (data, timestamp, sequenceNumber, audioLevel, duration, frameCount)
51
+ private var onAudioChunk: ((ByteArray, Double, Int, Float, Double, Int) -> Unit)? = null
48
52
  private var onStarted: ((timestamp: Double, sampleRate: Int, channels: Int, bitrate: Int, frameSize: Double, preSkip: Int) -> Unit)? = null
49
53
  private var onEnd: ((timestamp: Double, totalDuration: Double, totalPackets: Int) -> Unit)? = null
50
54
 
@@ -138,7 +142,7 @@ class AudioProcessor(private val config: AudioConfig) {
138
142
 
139
143
  // MARK: - Event callback setters
140
144
 
141
- fun setOnAudioChunk(callback: (ByteArray, Double, Int, Float) -> Unit) {
145
+ fun setOnAudioChunk(callback: (ByteArray, Double, Int, Float, Double, Int) -> Unit) {
142
146
  this.onAudioChunk = callback
143
147
  }
144
148
 
@@ -179,6 +183,7 @@ class AudioProcessor(private val config: AudioConfig) {
179
183
  fos.write(bytes)
180
184
  }
181
185
 
186
+ // Encode single frame to Opus
182
187
  val opusData = try {
183
188
  encoder.encode(frameData, samplesPerFrame)
184
189
  } catch (e: Exception) {
@@ -191,7 +196,11 @@ class AudioProcessor(private val config: AudioConfig) {
191
196
  continue
192
197
  }
193
198
 
194
- // Accumulate energy for RMS over ~360ms window
199
+ // Accumulate encoded frame into packet buffer
200
+ packetBuffer.write(opusData)
201
+ packetFrameCount++
202
+
203
+ // Accumulate energy for RMS level
195
204
  for (sample in frameData) {
196
205
  val s = sample.toDouble() / 32768.0
197
206
  levelSumSquares += s * s
@@ -208,20 +217,29 @@ class AudioProcessor(private val config: AudioConfig) {
208
217
  levelSampleCount = 0
209
218
  }
210
219
 
211
- val timestampMs = System.currentTimeMillis().toDouble()
212
- onAudioChunk?.invoke(opusData, timestampMs, sequenceNumber, currentLevel)
213
- sequenceNumber++
220
+ // Emit when we have enough frames for one packet (packetDuration)
221
+ if (packetFrameCount >= framesPerPacket) {
222
+ val timestampMs = System.currentTimeMillis().toDouble()
223
+ val duration = packetFrameCount * config.frameSize
224
+ onAudioChunk?.invoke(packetBuffer.toByteArray(), timestampMs, sequenceNumber, currentLevel, duration, packetFrameCount)
225
+ sequenceNumber++
226
+ packetBuffer.reset()
227
+ packetFrameCount = 0
228
+ }
214
229
  }
215
230
  }
216
231
 
217
232
  private fun _flushRemainingFrames() {
218
233
  val encoder = opusEncoder ?: return
219
- if (pendingSamples.isEmpty()) return
220
234
 
221
- while (pendingSamples.size < samplesPerFrame) {
222
- pendingSamples.add(0)
235
+ // Pad remaining PCM with silence to fill the last frame
236
+ if (pendingSamples.isNotEmpty() && pendingSamples.size < samplesPerFrame) {
237
+ while (pendingSamples.size < samplesPerFrame) {
238
+ pendingSamples.add(0)
239
+ }
223
240
  }
224
241
 
242
+ // Encode remaining frames
225
243
  while (pendingSamples.size >= samplesPerFrame) {
226
244
  val frameData = ShortArray(samplesPerFrame)
227
245
  for (i in 0 until samplesPerFrame) {
@@ -235,10 +253,18 @@ class AudioProcessor(private val config: AudioConfig) {
235
253
  }
236
254
 
237
255
  if (opusData == null || opusData.isEmpty()) continue
256
+ packetBuffer.write(opusData)
257
+ packetFrameCount++
258
+ }
238
259
 
260
+ // Flush any remaining packet buffer (even if less than framesPerPacket)
261
+ if (packetBuffer.size() > 0) {
239
262
  val timestampMs = System.currentTimeMillis().toDouble()
240
- onAudioChunk?.invoke(opusData, timestampMs, sequenceNumber, currentLevel)
263
+ val duration = packetFrameCount * config.frameSize
264
+ onAudioChunk?.invoke(packetBuffer.toByteArray(), timestampMs, sequenceNumber, currentLevel, duration, packetFrameCount)
241
265
  sequenceNumber++
266
+ packetBuffer.reset()
267
+ packetFrameCount = 0
242
268
  }
243
269
  }
244
270
  }
@@ -37,7 +37,7 @@ class AudioRecordManager(
37
37
  private var loggedFirstBuffer = false
38
38
 
39
39
  // Event callbacks
40
- private var onAudioChunk: ((ByteArray, Double, Int, Float) -> Unit)? = null
40
+ private var onAudioChunk: ((ByteArray, Double, Int, Float, Double, Int) -> Unit)? = null
41
41
  private var onStarted: ((timestamp: Double, sampleRate: Int, channels: Int, bitrate: Int, frameSize: Double, preSkip: Int) -> Unit)? = null
42
42
  private var onEnd: ((timestamp: Double, totalDuration: Double, totalPackets: Int) -> Unit)? = null
43
43
  private var onAmplitude: ((Float, Float, Double) -> Unit)? = null
@@ -91,8 +91,8 @@ class AudioRecordManager(
91
91
 
92
92
  // Create and start AudioProcessor (encoding thread)
93
93
  val proc = AudioProcessor(config)
94
- proc.setOnAudioChunk { data, timestamp, seq, level ->
95
- onAudioChunk?.invoke(data, timestamp, seq, level)
94
+ proc.setOnAudioChunk { data, timestamp, seq, level, duration, frameCount ->
95
+ onAudioChunk?.invoke(data, timestamp, seq, level, duration, frameCount)
96
96
  }
97
97
  proc.setOnStarted { timestamp, sampleRate, channels, bitrate, frameSize, preSkip ->
98
98
  onStarted?.invoke(timestamp, sampleRate, channels, bitrate, frameSize, preSkip)
@@ -172,7 +172,7 @@ class AudioRecordManager(
172
172
  }
173
173
 
174
174
  // Event handlers
175
- fun setOnAudioChunk(callback: (ByteArray, Double, Int, Float) -> Unit) {
175
+ fun setOnAudioChunk(callback: (ByteArray, Double, Int, Float, Double, Int) -> Unit) {
176
176
  this.onAudioChunk = callback
177
177
  }
178
178
 
@@ -81,12 +81,14 @@ class OpuslibModule : Module() {
81
81
 
82
82
  // Set up event callbacks — audioStarted/audioEnd come from encoding thread
83
83
  android.util.Log.d(TAG, "🔗 Setting up event callbacks...")
84
- manager.setOnAudioChunk { data, timestamp, sequenceNumber, audioLevel ->
84
+ manager.setOnAudioChunk { data, timestamp, sequenceNumber, audioLevel, duration, frameCount ->
85
85
  sendEvent("audioChunk", mapOf(
86
86
  "data" to data,
87
87
  "timestamp" to timestamp,
88
88
  "sequenceNumber" to sequenceNumber,
89
- "audioLevel" to audioLevel
89
+ "audioLevel" to audioLevel,
90
+ "duration" to duration,
91
+ "frameCount" to frameCount
90
92
  ))
91
93
  }
92
94
 
@@ -35,6 +35,10 @@ export interface AudioChunkEvent {
35
35
  sequenceNumber: number;
36
36
  /** Audio level normalized to 0.0~1.0 (mapped from dBFS, 0 = silence, 1 = loud) */
37
37
  audioLevel: number;
38
+ /** Duration of this packet in milliseconds (frameSize * frameCount) */
39
+ duration: number;
40
+ /** Number of Opus frames in this packet */
41
+ frameCount: number;
38
42
  }
39
43
  /**
40
44
  * Amplitude event payload (for waveform visualization)
@@ -1 +1 @@
1
- {"version":3,"file":"Opuslib.types.d.ts","sourceRoot":"","sources":["../src/Opuslib.types.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,2DAA2D;IAC3D,UAAU,EAAE,MAAM,CAAA;IAClB,gDAAgD;IAChD,QAAQ,EAAE,MAAM,CAAA;IAChB,6DAA6D;IAC7D,OAAO,EAAE,MAAM,CAAA;IACf,8DAA8D;IAC9D,SAAS,EAAE,MAAM,CAAA;IACjB,2DAA2D;IAC3D,cAAc,EAAE,MAAM,CAAA;IACtB,oFAAoF;IACpF,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,yDAAyD;IACzD,qBAAqB,CAAC,EAAE,OAAO,CAAA;IAC/B,4DAA4D;IAC5D,sBAAsB,CAAC,EAAE,MAAM,CAAA;IAC/B,oEAAoE;IACpE,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,sDAAsD;IACtD,cAAc,CAAC,EAAE,OAAO,CAAA;CACzB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,6CAA6C;IAC7C,IAAI,EAAE,WAAW,CAAA;IACjB,gCAAgC;IAChC,SAAS,EAAE,MAAM,CAAA;IACjB,oDAAoD;IACpD,cAAc,EAAE,MAAM,CAAA;IACtB,kFAAkF;IAClF,UAAU,EAAE,MAAM,CAAA;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,6CAA6C;IAC7C,GAAG,EAAE,MAAM,CAAA;IACX,iCAAiC;IACjC,IAAI,EAAE,MAAM,CAAA;IACZ,gCAAgC;IAChC,SAAS,EAAE,MAAM,CAAA;CAClB;AAED;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC,uDAAuD;IACvD,SAAS,EAAE,MAAM,CAAA;IACjB,oCAAoC;IACpC,UAAU,EAAE,MAAM,CAAA;IAClB,yBAAyB;IACzB,QAAQ,EAAE,MAAM,CAAA;IAChB,wCAAwC;IACxC,OAAO,EAAE,MAAM,CAAA;IACf,qCAAqC;IACrC,SAAS,EAAE,MAAM,CAAA;IACjB,yFAAyF;IACzF,OAAO,EAAE,MAAM,CAAA;CAChB;AAED;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,uDAAuD;IACvD,SAAS,EAAE,MAAM,CAAA;IACjB,8DAA8D;IAC9D,aAAa,EAAE,MAAM,CAAA;IACrB,yDAAyD;IACzD,YAAY,EAAE,MAAM,CAAA;CACrB;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,iBAAiB;IACjB,IAAI,EAAE,MAAM,CAAA;IACZ,oBAAoB;IACpB,OAAO,EAAE,MAAM,CAAA;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,IAAI,CAAA;CACnB"}
1
+ {"version":3,"file":"Opuslib.types.d.ts","sourceRoot":"","sources":["../src/Opuslib.types.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,2DAA2D;IAC3D,UAAU,EAAE,MAAM,CAAA;IAClB,gDAAgD;IAChD,QAAQ,EAAE,MAAM,CAAA;IAChB,6DAA6D;IAC7D,OAAO,EAAE,MAAM,CAAA;IACf,8DAA8D;IAC9D,SAAS,EAAE,MAAM,CAAA;IACjB,2DAA2D;IAC3D,cAAc,EAAE,MAAM,CAAA;IACtB,oFAAoF;IACpF,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,yDAAyD;IACzD,qBAAqB,CAAC,EAAE,OAAO,CAAA;IAC/B,4DAA4D;IAC5D,sBAAsB,CAAC,EAAE,MAAM,CAAA;IAC/B,oEAAoE;IACpE,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,sDAAsD;IACtD,cAAc,CAAC,EAAE,OAAO,CAAA;CACzB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,6CAA6C;IAC7C,IAAI,EAAE,WAAW,CAAA;IACjB,gCAAgC;IAChC,SAAS,EAAE,MAAM,CAAA;IACjB,oDAAoD;IACpD,cAAc,EAAE,MAAM,CAAA;IACtB,kFAAkF;IAClF,UAAU,EAAE,MAAM,CAAA;IAClB,uEAAuE;IACvE,QAAQ,EAAE,MAAM,CAAA;IAChB,2CAA2C;IAC3C,UAAU,EAAE,MAAM,CAAA;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,6CAA6C;IAC7C,GAAG,EAAE,MAAM,CAAA;IACX,iCAAiC;IACjC,IAAI,EAAE,MAAM,CAAA;IACZ,gCAAgC;IAChC,SAAS,EAAE,MAAM,CAAA;CAClB;AAED;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC,uDAAuD;IACvD,SAAS,EAAE,MAAM,CAAA;IACjB,oCAAoC;IACpC,UAAU,EAAE,MAAM,CAAA;IAClB,yBAAyB;IACzB,QAAQ,EAAE,MAAM,CAAA;IAChB,wCAAwC;IACxC,OAAO,EAAE,MAAM,CAAA;IACf,qCAAqC;IACrC,SAAS,EAAE,MAAM,CAAA;IACjB,yFAAyF;IACzF,OAAO,EAAE,MAAM,CAAA;CAChB;AAED;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,uDAAuD;IACvD,SAAS,EAAE,MAAM,CAAA;IACjB,8DAA8D;IAC9D,aAAa,EAAE,MAAM,CAAA;IACrB,yDAAyD;IACzD,YAAY,EAAE,MAAM,CAAA;CACrB;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,iBAAiB;IACjB,IAAI,EAAE,MAAM,CAAA;IACZ,oBAAoB;IACpB,OAAO,EAAE,MAAM,CAAA;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,IAAI,CAAA;CACnB"}
@@ -1 +1 @@
1
- {"version":3,"file":"Opuslib.types.js","sourceRoot":"","sources":["../src/Opuslib.types.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * Audio configuration for Opus encoding\n */\nexport interface AudioConfig {\n /** Sample rate in Hz (8000, 12000, 16000, 24000, 48000) */\n sampleRate: number\n /** Number of channels (1 = mono, 2 = stereo) */\n channels: number\n /** Target bitrate in bits/second (e.g., 24000 for 24kbps) */\n bitrate: number\n /** Frame duration in milliseconds (2.5, 5, 10, 20, 40, 60) */\n frameSize: number\n /** Packet duration in milliseconds (typically 20-100ms) */\n packetDuration: number\n /** DRED recovery duration in milliseconds (0-100, default 100) - NEW in Opus 1.6 */\n dredDuration?: number\n /** Enable amplitude events for waveform visualization */\n enableAmplitudeEvents?: boolean\n /** Amplitude event interval in milliseconds (default 16) */\n amplitudeEventInterval?: number\n /** Audio level RMS window duration in milliseconds (default 360) */\n audioLevelWindow?: number\n /** Save debug PCM audio to file (development only) */\n saveDebugAudio?: boolean\n}\n\n/**\n * Audio chunk event payload (Opus-encoded data)\n */\nexport interface AudioChunkEvent {\n /** Opus-encoded audio data as ArrayBuffer */\n data: ArrayBuffer\n /** Timestamp in milliseconds */\n timestamp: number\n /** Sequence number (increments with each packet) */\n sequenceNumber: number\n /** Audio level normalized to 0.0~1.0 (mapped from dBFS, 0 = silence, 1 = loud) */\n audioLevel: number\n}\n\n/**\n * Amplitude event payload (for waveform visualization)\n */\nexport interface AmplitudeEvent {\n /** Root mean square amplitude (0.0 - 1.0) */\n rms: number\n /** Peak amplitude (0.0 - 1.0) */\n peak: number\n /** Timestamp in milliseconds */\n timestamp: number\n}\n\n/**\n * Audio started event payload\n * Emitted when audio streaming successfully starts\n */\nexport interface AudioStartedEvent {\n /** Timestamp in milliseconds when streaming started */\n timestamp: number\n /** Actual sample rate being used */\n sampleRate: number\n /** Number of channels */\n channels: number\n /** Configured bitrate in bits/second */\n bitrate: number\n /** Frame duration in milliseconds */\n frameSize: number\n /** Opus encoder lookahead in samples (decoder should skip this many samples at start) */\n preSkip: number\n}\n\n/**\n * Audio end event payload\n * Emitted when audio streaming stops\n */\nexport interface AudioEndEvent {\n /** Timestamp in milliseconds when streaming stopped */\n timestamp: number\n /** Total duration of the streaming session in milliseconds */\n totalDuration: number\n /** Total number of packets encoded during the session */\n totalPackets: number\n}\n\n/**\n * Error event payload\n */\nexport interface ErrorEvent {\n /** Error code */\n code: string\n /** Error message */\n message: string\n}\n\n/**\n * Event subscription\n */\nexport interface Subscription {\n remove: () => void\n}\n"]}
1
+ {"version":3,"file":"Opuslib.types.js","sourceRoot":"","sources":["../src/Opuslib.types.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * Audio configuration for Opus encoding\n */\nexport interface AudioConfig {\n /** Sample rate in Hz (8000, 12000, 16000, 24000, 48000) */\n sampleRate: number\n /** Number of channels (1 = mono, 2 = stereo) */\n channels: number\n /** Target bitrate in bits/second (e.g., 24000 for 24kbps) */\n bitrate: number\n /** Frame duration in milliseconds (2.5, 5, 10, 20, 40, 60) */\n frameSize: number\n /** Packet duration in milliseconds (typically 20-100ms) */\n packetDuration: number\n /** DRED recovery duration in milliseconds (0-100, default 100) - NEW in Opus 1.6 */\n dredDuration?: number\n /** Enable amplitude events for waveform visualization */\n enableAmplitudeEvents?: boolean\n /** Amplitude event interval in milliseconds (default 16) */\n amplitudeEventInterval?: number\n /** Audio level RMS window duration in milliseconds (default 360) */\n audioLevelWindow?: number\n /** Save debug PCM audio to file (development only) */\n saveDebugAudio?: boolean\n}\n\n/**\n * Audio chunk event payload (Opus-encoded data)\n */\nexport interface AudioChunkEvent {\n /** Opus-encoded audio data as ArrayBuffer */\n data: ArrayBuffer\n /** Timestamp in milliseconds */\n timestamp: number\n /** Sequence number (increments with each packet) */\n sequenceNumber: number\n /** Audio level normalized to 0.0~1.0 (mapped from dBFS, 0 = silence, 1 = loud) */\n audioLevel: number\n /** Duration of this packet in milliseconds (frameSize * frameCount) */\n duration: number\n /** Number of Opus frames in this packet */\n frameCount: number\n}\n\n/**\n * Amplitude event payload (for waveform visualization)\n */\nexport interface AmplitudeEvent {\n /** Root mean square amplitude (0.0 - 1.0) */\n rms: number\n /** Peak amplitude (0.0 - 1.0) */\n peak: number\n /** Timestamp in milliseconds */\n timestamp: number\n}\n\n/**\n * Audio started event payload\n * Emitted when audio streaming successfully starts\n */\nexport interface AudioStartedEvent {\n /** Timestamp in milliseconds when streaming started */\n timestamp: number\n /** Actual sample rate being used */\n sampleRate: number\n /** Number of channels */\n channels: number\n /** Configured bitrate in bits/second */\n bitrate: number\n /** Frame duration in milliseconds */\n frameSize: number\n /** Opus encoder lookahead in samples (decoder should skip this many samples at start) */\n preSkip: number\n}\n\n/**\n * Audio end event payload\n * Emitted when audio streaming stops\n */\nexport interface AudioEndEvent {\n /** Timestamp in milliseconds when streaming stopped */\n timestamp: number\n /** Total duration of the streaming session in milliseconds */\n totalDuration: number\n /** Total number of packets encoded during the session */\n totalPackets: number\n}\n\n/**\n * Error event payload\n */\nexport interface ErrorEvent {\n /** Error code */\n code: string\n /** Error message */\n message: string\n}\n\n/**\n * Event subscription\n */\nexport interface Subscription {\n remove: () => void\n}\n"]}
@@ -29,7 +29,7 @@ class AudioEngineManager {
29
29
  private var loggedFirstBuffer = false
30
30
 
31
31
  // Event callbacks
32
- private var onAudioChunk: ((Data, Double, Int, Float) -> Void)?
32
+ private var onAudioChunk: ((Data, Double, Int, Float, Double, Int) -> Void)?
33
33
  private var onStarted: ((_ timestamp: Double, _ sampleRate: Int, _ channels: Int, _ bitrate: Int, _ frameSize: Double, _ preSkip: Int) -> Void)?
34
34
  private var onEnd: ((_ timestamp: Double, _ totalDuration: Double, _ totalPackets: Int) -> Void)?
35
35
  private var onAmplitude: ((Float, Float, Double) -> Void)?
@@ -59,8 +59,8 @@ class AudioEngineManager {
59
59
 
60
60
  // Create and start AudioProcessor (encoding thread)
61
61
  let proc = AudioProcessor(config: config)
62
- proc.setOnAudioChunk { [weak self] data, timestamp, seq, level in
63
- self?.onAudioChunk?(data, timestamp, seq, level)
62
+ proc.setOnAudioChunk { [weak self] data, timestamp, seq, level, duration, frameCount in
63
+ self?.onAudioChunk?(data, timestamp, seq, level, duration, frameCount)
64
64
  }
65
65
  proc.setOnStarted { [weak self] timestamp, sampleRate, channels, bitrate, frameSize, preSkip in
66
66
  self?.onStarted?(timestamp, sampleRate, channels, bitrate, frameSize, preSkip)
@@ -159,7 +159,7 @@ class AudioEngineManager {
159
159
 
160
160
  // MARK: - Event Handlers
161
161
 
162
- func setOnAudioChunk(_ callback: @escaping (Data, Double, Int, Float) -> Void) {
162
+ func setOnAudioChunk(_ callback: @escaping (Data, Double, Int, Float, Double, Int) -> Void) {
163
163
  self.onAudioChunk = callback
164
164
  }
165
165
 
@@ -19,6 +19,9 @@ class AudioProcessor {
19
19
  private var opusEncoder: OpusEncoder?
20
20
  private var pendingSamples: [Int16] = []
21
21
  private let samplesPerFrame: Int
22
+ private let framesPerPacket: Int // how many frames to batch before emitting
23
+ private var packetBuffer: Data = Data() // accumulates encoded frames
24
+ private var packetFrameCount: Int = 0
22
25
  private var sequenceNumber: Int = 0
23
26
  private var startTime: Double = 0
24
27
 
@@ -32,7 +35,8 @@ class AudioProcessor {
32
35
  private var pcmFileHandle: FileHandle?
33
36
 
34
37
  // Event callbacks (all invoked on encoding queue)
35
- private var onAudioChunk: ((Data, Double, Int, Float) -> Void)?
38
+ // onAudioChunk: (data, timestamp, sequenceNumber, audioLevel, duration, frameCount)
39
+ private var onAudioChunk: ((Data, Double, Int, Float, Double, Int) -> Void)?
36
40
  private var onStarted: ((_ timestamp: Double, _ sampleRate: Int, _ channels: Int, _ bitrate: Int, _ frameSize: Double, _ preSkip: Int) -> Void)?
37
41
  private var onEnd: ((_ timestamp: Double, _ totalDuration: Double, _ totalPackets: Int) -> Void)?
38
42
 
@@ -42,6 +46,7 @@ class AudioProcessor {
42
46
  init(config: AudioConfig) {
43
47
  self.config = config
44
48
  self.samplesPerFrame = Int(Double(config.sampleRate) * config.frameSize / 1000.0)
49
+ self.framesPerPacket = max(1, Int(config.packetDuration / config.frameSize))
45
50
  let windowMs = config.audioLevelWindow ?? 360
46
51
  self.levelUpdateSamples = config.sampleRate * config.channels * windowMs / 1000
47
52
  }
@@ -128,7 +133,7 @@ class AudioProcessor {
128
133
 
129
134
  // MARK: - Event callbacks
130
135
 
131
- func setOnAudioChunk(_ callback: @escaping (Data, Double, Int, Float) -> Void) {
136
+ func setOnAudioChunk(_ callback: @escaping (Data, Double, Int, Float, Double, Int) -> Void) {
132
137
  self.onAudioChunk = callback
133
138
  }
134
139
 
@@ -157,7 +162,7 @@ class AudioProcessor {
157
162
  }
158
163
  }
159
164
 
160
- // Encode to Opus
165
+ // Encode single frame to Opus
161
166
  var encodedPacket: Data?
162
167
  frameData.withUnsafeBufferPointer { bufferPointer in
163
168
  guard let baseAddress = bufferPointer.baseAddress else { return }
@@ -169,7 +174,11 @@ class AudioProcessor {
169
174
  continue
170
175
  }
171
176
 
172
- // Accumulate energy for RMS over ~360ms window
177
+ // Accumulate encoded frame into packet buffer
178
+ packetBuffer.append(opusData)
179
+ packetFrameCount += 1
180
+
181
+ // Accumulate energy for RMS level
173
182
  for sample in frameData {
174
183
  let s = Double(sample) / 32768.0
175
184
  levelSumSquares += s * s
@@ -186,20 +195,27 @@ class AudioProcessor {
186
195
  levelSampleCount = 0
187
196
  }
188
197
 
189
- let timestampMs = Date().timeIntervalSince1970 * 1000
190
- onAudioChunk?(opusData, timestampMs, sequenceNumber, currentLevel)
191
- sequenceNumber += 1
198
+ // Emit when we have enough frames for one packet (packetDuration)
199
+ if packetFrameCount >= framesPerPacket {
200
+ let timestampMs = Date().timeIntervalSince1970 * 1000
201
+ let duration = Double(packetFrameCount) * config.frameSize
202
+ onAudioChunk?(packetBuffer, timestampMs, sequenceNumber, currentLevel, duration, packetFrameCount)
203
+ sequenceNumber += 1
204
+ packetBuffer = Data()
205
+ packetFrameCount = 0
206
+ }
192
207
  }
193
208
  }
194
209
 
195
210
  private func _flushRemainingFrames() {
196
211
  guard let opusEncoder = opusEncoder else { return }
197
- guard !pendingSamples.isEmpty else { return }
198
212
 
199
- if pendingSamples.count < samplesPerFrame {
213
+ // Pad remaining PCM with silence to fill the last frame
214
+ if !pendingSamples.isEmpty && pendingSamples.count < samplesPerFrame {
200
215
  pendingSamples.append(contentsOf: [Int16](repeating: 0, count: samplesPerFrame - pendingSamples.count))
201
216
  }
202
217
 
218
+ // Encode remaining frames
203
219
  while pendingSamples.count >= samplesPerFrame {
204
220
  let frameData = Array(pendingSamples.prefix(samplesPerFrame))
205
221
  pendingSamples.removeFirst(samplesPerFrame)
@@ -211,10 +227,18 @@ class AudioProcessor {
211
227
  }
212
228
 
213
229
  guard let opusData = encodedPacket, !opusData.isEmpty else { continue }
230
+ packetBuffer.append(opusData)
231
+ packetFrameCount += 1
232
+ }
214
233
 
234
+ // Flush any remaining packet buffer (even if less than framesPerPacket)
235
+ if !packetBuffer.isEmpty {
215
236
  let timestampMs = Date().timeIntervalSince1970 * 1000
216
- onAudioChunk?(opusData, timestampMs, sequenceNumber, currentLevel)
237
+ let duration = Double(packetFrameCount) * config.frameSize
238
+ onAudioChunk?(packetBuffer, timestampMs, sequenceNumber, currentLevel, duration, packetFrameCount)
217
239
  sequenceNumber += 1
240
+ packetBuffer = Data()
241
+ packetFrameCount = 0
218
242
  }
219
243
  }
220
244
  }
@@ -58,12 +58,14 @@ public class OpuslibModule: Module {
58
58
  print("[OpuslibModule] ✅ AudioEngineManager created")
59
59
 
60
60
  // Set up event callbacks — audioStarted/audioEnd come from encoding thread
61
- manager.setOnAudioChunk { [weak self] data, timestamp, sequenceNumber, audioLevel in
61
+ manager.setOnAudioChunk { [weak self] data, timestamp, sequenceNumber, audioLevel, duration, frameCount in
62
62
  self?.sendEvent("audioChunk", [
63
63
  "data": data,
64
64
  "timestamp": timestamp,
65
65
  "sequenceNumber": sequenceNumber,
66
- "audioLevel": audioLevel
66
+ "audioLevel": audioLevel,
67
+ "duration": duration,
68
+ "frameCount": frameCount
67
69
  ])
68
70
  }
69
71
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@imcooder/opuslib",
3
- "version": "0.2.1",
3
+ "version": "2.2.1",
4
4
  "description": "Opus 1.6 audio encoding for React Native and Expo with audio level metering and lifecycle events. Forked from Scdales/opuslib.",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -36,6 +36,10 @@ export interface AudioChunkEvent {
36
36
  sequenceNumber: number
37
37
  /** Audio level normalized to 0.0~1.0 (mapped from dBFS, 0 = silence, 1 = loud) */
38
38
  audioLevel: number
39
+ /** Duration of this packet in milliseconds (frameSize * frameCount) */
40
+ duration: number
41
+ /** Number of Opus frames in this packet */
42
+ frameCount: number
39
43
  }
40
44
 
41
45
  /**