@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 +21 -9
- package/android/src/main/java/expo/modules/opuslib/AudioProcessor.kt +36 -10
- package/android/src/main/java/expo/modules/opuslib/AudioRecordManager.kt +4 -4
- package/android/src/main/java/expo/modules/opuslib/OpuslibModule.kt +4 -2
- package/build/Opuslib.types.d.ts +4 -0
- package/build/Opuslib.types.d.ts.map +1 -1
- package/build/Opuslib.types.js.map +1 -1
- package/ios/AudioEngineManager.swift +4 -4
- package/ios/AudioProcessor.swift +34 -10
- package/ios/OpuslibModule.swift +4 -2
- package/package.json +1 -1
- package/src/Opuslib.types.ts +4 -0
package/README.md
CHANGED
|
@@ -29,28 +29,36 @@
|
|
|
29
29
|
> });
|
|
30
30
|
> ```
|
|
31
31
|
>
|
|
32
|
-
>
|
|
33
|
-
> -
|
|
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
|
|
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
|
|
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:
|
|
53
|
-
> audioLevelWindow: 200,
|
|
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 -
|
|
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; //
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
222
|
-
|
|
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
|
-
|
|
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
|
|
package/build/Opuslib.types.d.ts
CHANGED
|
@@ -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
|
|
package/ios/AudioProcessor.swift
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/ios/OpuslibModule.swift
CHANGED
|
@@ -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": "
|
|
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",
|
package/src/Opuslib.types.ts
CHANGED
|
@@ -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
|
/**
|