opuslib 0.1.4 → 0.2.0

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.
@@ -6,19 +6,16 @@ import android.media.AudioRecord
6
6
  import android.media.MediaRecorder
7
7
  import android.util.Log
8
8
  import java.io.File
9
- import java.io.FileOutputStream
10
- import java.nio.ByteBuffer
11
- import java.nio.ByteOrder
12
9
  import kotlin.concurrent.thread
13
10
 
14
11
  /**
15
- * AudioRecordManager - Manages AudioRecord for real-time audio capture with Opus 1.6.1 DRED
12
+ * AudioRecordManager - Manages AudioRecord for real-time audio capture.
16
13
  *
17
- * This class handles:
18
- * - AudioRecord setup and lifecycle
19
- * - Real-time PCM audio capture
20
- * - Opus 1.6.1 encoding with DRED support
21
- * - Background recording thread management
14
+ * This class is responsible only for audio capture: it owns the AudioRecord
15
+ * lifecycle and runs the PCM read loop. All Opus 1.6.1 encoding is delegated to
16
+ * AudioProcessor, which runs on a separate dedicated encoding thread. The
17
+ * capture thread calls processor.pushSamples(), which copies the PCM and posts
18
+ * it to the encoding thread, so the read loop never blocks on encoding.
22
19
  */
23
20
  class AudioRecordManager(
24
21
  private val context: Context,
@@ -32,28 +29,21 @@ class AudioRecordManager(
32
29
  private var audioRecord: AudioRecord? = null
33
30
  private var recordingThread: Thread? = null
34
31
 
35
- // Opus encoder
36
- private var opusEncoder: OpusEncoder? = null
32
+ // Encoding processor (owns the encoder, runs on a separate HandlerThread)
33
+ private var processor: AudioProcessor? = null
37
34
 
38
35
  // State
39
36
  private var isRecording = false
40
37
  private var isPaused = false
41
- private var sequenceNumber = 0
42
38
  private var loggedFirstBuffer = false
43
39
 
44
- // Frame accumulation for packet duration
45
- private val frameBuffer = mutableListOf<ShortArray>()
46
- private val framesPerPacket: Int = (config.packetDuration / config.frameSize).toInt()
47
-
48
40
  // Event callbacks
49
- private var onAudioChunk: ((ByteArray, Double, Int) -> Unit)? = null
41
+ private var onAudioChunk: ((List<EncodedFrame>, Double, Int, Double, Int) -> Unit)? = null
42
+ private var onStarted: ((timestamp: Double, sampleRate: Int, channels: Int, bitrate: Int, frameSize: Double, preSkip: Int) -> Unit)? = null
43
+ private var onEnd: ((timestamp: Double, totalDuration: Double, totalPackets: Int) -> Unit)? = null
50
44
  private var onAmplitude: ((Float, Float, Double) -> Unit)? = null
51
45
  private var onError: ((Exception) -> Unit)? = null
52
46
 
53
- // Debug file output
54
- private var pcmFileOutputStream: FileOutputStream? = null
55
- private var pcmFile: File? = null
56
-
57
47
  // MARK: - Public Methods
58
48
 
59
49
  fun start() {
@@ -61,16 +51,6 @@ class AudioRecordManager(
61
51
  throw AudioStreamException("ALREADY_STREAMING", "Already recording")
62
52
  }
63
53
 
64
- // Create Opus encoder with DRED support
65
- val dredDuration = config.dredDuration
66
- opusEncoder = OpusEncoder(
67
- sampleRate = config.sampleRate,
68
- channels = config.channels,
69
- bitrate = config.bitrate,
70
- frameSizeMs = config.frameSize,
71
- dredDurationMs = dredDuration
72
- )
73
-
74
54
  // Calculate buffer size
75
55
  val samplesPerFrame = (config.sampleRate * config.frameSize / 1000.0).toInt()
76
56
  val bufferSize = samplesPerFrame * 2 // 2 bytes per sample (Int16)
@@ -118,33 +98,48 @@ class AudioRecordManager(
118
98
  )
119
99
  }
120
100
 
121
- // Create debug output file if enabled
122
- if (config.saveDebugAudio) {
123
- try {
124
- val timestamp = System.currentTimeMillis()
125
- pcmFile = File(context.filesDir, "debug_pcm_$timestamp.raw")
126
- pcmFileOutputStream = FileOutputStream(pcmFile)
127
- Log.d(TAG, "Debug PCM file created: ${pcmFile?.absolutePath}")
128
- } catch (e: Exception) {
129
- Log.e(TAG, "Failed to create debug file: ${e.message}")
130
- }
101
+ // Create and start AudioProcessor (encoding thread). It owns the encoder and
102
+ // emits audioStarted (with preSkip) once the encoder is ready.
103
+ val proc = AudioProcessor(config)
104
+ proc.setOnAudioChunk { frames, timestamp, seq, duration, frameCount ->
105
+ onAudioChunk?.invoke(frames, timestamp, seq, duration, frameCount)
131
106
  }
107
+ proc.setOnStarted { timestamp, sampleRate, channels, bitrate, frameSize, preSkip ->
108
+ onStarted?.invoke(timestamp, sampleRate, channels, bitrate, frameSize, preSkip)
109
+ }
110
+ proc.setOnEnd { timestamp, totalDuration, totalPackets ->
111
+ onEnd?.invoke(timestamp, totalDuration, totalPackets)
112
+ }
113
+
114
+ // Debug PCM file (written on the encoding thread, per frame)
115
+ val debugFile = if (config.saveDebugAudio) {
116
+ val timestamp = System.currentTimeMillis()
117
+ File(context.filesDir, "debug_pcm_$timestamp.raw").also {
118
+ Log.d(TAG, "Debug PCM file: ${it.absolutePath}")
119
+ }
120
+ } else null
121
+
122
+ proc.start(debugFile)
123
+ processor = proc
132
124
 
133
125
  // Start recording
134
126
  try {
135
127
  record.startRecording()
136
128
  } catch (e: Exception) {
129
+ // Tear down the encoding thread we just started before bailing out
130
+ proc.flushAndStop()
131
+ processor = null
137
132
  throw AudioStreamException("AUDIO_RECORD_ERROR", "Failed to start recording: ${e.message}")
138
133
  }
139
134
 
140
135
  isRecording = true
141
136
 
142
- // Start recording thread
137
+ // Start capture thread — only reads PCM and pushes to the processor
143
138
  recordingThread = thread(start = true, name = "AudioRecordThread") {
144
- recordAudioLoop(record, samplesPerFrame)
139
+ captureLoop(record, samplesPerFrame)
145
140
  }
146
141
 
147
- Log.d(TAG, "Started recording: ${config.sampleRate}Hz, ${config.channels}ch, DRED: ${dredDuration}ms")
142
+ Log.d(TAG, "Started recording: ${config.sampleRate}Hz, ${config.channels}ch, DRED: ${config.dredDuration}ms")
148
143
  }
149
144
 
150
145
  fun stop() {
@@ -154,7 +149,7 @@ class AudioRecordManager(
154
149
 
155
150
  isRecording = false
156
151
 
157
- // Stop recording
152
+ // Stop AudioRecord
158
153
  audioRecord?.let { record ->
159
154
  try {
160
155
  if (record.recordingState == AudioRecord.RECORDSTATE_RECORDING) {
@@ -165,26 +160,20 @@ class AudioRecordManager(
165
160
  }
166
161
  }
167
162
 
168
- // Wait for recording thread to finish
163
+ // Wait for capture thread to finish
169
164
  recordingThread?.join(1000)
170
165
  recordingThread = null
171
166
 
172
- // Release resources
167
+ // Flush and stop the encoding thread (synchronous — drains remaining
168
+ // samples, pads the final partial frame with silence, emits audioEnd).
169
+ processor?.flushAndStop()
170
+ processor = null
171
+
172
+ // Release AudioRecord
173
173
  audioRecord?.release()
174
174
  audioRecord = null
175
175
 
176
- opusEncoder?.destroy()
177
- opusEncoder = null
178
-
179
- frameBuffer.clear()
180
- sequenceNumber = 0
181
-
182
- // Close debug file
183
- pcmFileOutputStream?.close()
184
- pcmFileOutputStream = null
185
- if (pcmFile != null) {
186
- Log.d(TAG, "Closed PCM debug file: ${pcmFile?.absolutePath}")
187
- }
176
+ loggedFirstBuffer = false
188
177
 
189
178
  Log.d(TAG, "Stopped recording")
190
179
  }
@@ -201,10 +190,18 @@ class AudioRecordManager(
201
190
 
202
191
  // MARK: - Event Handlers
203
192
 
204
- fun setOnAudioChunk(callback: (ByteArray, Double, Int) -> Unit) {
193
+ fun setOnAudioChunk(callback: (List<EncodedFrame>, Double, Int, Double, Int) -> Unit) {
205
194
  this.onAudioChunk = callback
206
195
  }
207
196
 
197
+ fun setOnStarted(callback: (timestamp: Double, sampleRate: Int, channels: Int, bitrate: Int, frameSize: Double, preSkip: Int) -> Unit) {
198
+ this.onStarted = callback
199
+ }
200
+
201
+ fun setOnEnd(callback: (timestamp: Double, totalDuration: Double, totalPackets: Int) -> Unit) {
202
+ this.onEnd = callback
203
+ }
204
+
208
205
  fun setOnAmplitude(callback: (Float, Float, Double) -> Unit) {
209
206
  this.onAmplitude = callback
210
207
  }
@@ -213,12 +210,12 @@ class AudioRecordManager(
213
210
  this.onError = callback
214
211
  }
215
212
 
216
- // MARK: - Private Methods
213
+ // MARK: - Capture thread (only reads PCM, no encoding)
217
214
 
218
- private fun recordAudioLoop(record: AudioRecord, samplesPerFrame: Int) {
215
+ private fun captureLoop(record: AudioRecord, samplesPerFrame: Int) {
219
216
  val buffer = ShortArray(samplesPerFrame)
220
217
 
221
- Log.d(TAG, "Recording thread started, frame size: $samplesPerFrame samples")
218
+ Log.d(TAG, "Capture thread started, frame size: $samplesPerFrame samples")
222
219
 
223
220
  while (isRecording) {
224
221
  try {
@@ -232,7 +229,7 @@ class AudioRecordManager(
232
229
  }
233
230
 
234
231
  if (samplesRead == 0) {
235
- // No data available, continue
232
+ // No data available, wait briefly and retry
236
233
  Thread.sleep(10)
237
234
  continue
238
235
  }
@@ -248,99 +245,19 @@ class AudioRecordManager(
248
245
  continue
249
246
  }
250
247
 
251
- // Write to debug file if enabled
252
- pcmFileOutputStream?.let { fos ->
253
- val byteBuffer = ByteBuffer.allocate(samplesRead * 2)
254
- byteBuffer.order(ByteOrder.LITTLE_ENDIAN)
255
- for (i in 0 until samplesRead) {
256
- byteBuffer.putShort(buffer[i])
257
- }
258
- fos.write(byteBuffer.array())
259
- }
260
-
261
- // Process the buffer
262
- processBuffer(buffer.copyOf(samplesRead))
248
+ // Copy + post to the encoding thread (never blocks the capture loop)
249
+ processor?.pushSamples(buffer, samplesRead)
263
250
 
264
251
  } catch (e: InterruptedException) {
265
- Log.d(TAG, "Recording thread interrupted")
252
+ Log.d(TAG, "Capture thread interrupted")
266
253
  break
267
254
  } catch (e: Exception) {
268
- Log.e(TAG, "Error in recording loop: ${e.message}", e)
255
+ Log.e(TAG, "Error in capture loop: ${e.message}", e)
269
256
  onError?.invoke(e)
270
257
  break
271
258
  }
272
259
  }
273
260
 
274
- Log.d(TAG, "Recording thread stopped")
275
- }
276
-
277
- private fun processBuffer(pcmData: ShortArray) {
278
- // Add to frame buffer
279
- frameBuffer.add(pcmData)
280
-
281
- // Calculate how many samples we need for one packet
282
- val samplesPerPacket = (config.sampleRate * config.packetDuration / 1000.0).toInt()
283
- val currentSampleCount = frameBuffer.sumOf { it.size }
284
-
285
- // When we have enough samples for a packet, encode and send
286
- if (currentSampleCount >= samplesPerPacket) {
287
- encodeAndSendPacket()
288
- }
289
- }
290
-
291
- private fun encodeAndSendPacket() {
292
- val encoder = opusEncoder ?: return
293
-
294
- // Flatten frame buffer into continuous PCM data
295
- val pcmData = ShortArray(frameBuffer.sumOf { it.size })
296
- var offset = 0
297
- for (frame in frameBuffer) {
298
- frame.copyInto(pcmData, offset)
299
- offset += frame.size
300
- }
301
-
302
- // Calculate samples per frame
303
- val samplesPerFrame = (config.sampleRate * config.frameSize / 1000.0).toInt()
304
-
305
- // We should only encode when we have at least one frame
306
- if (pcmData.size < samplesPerFrame) {
307
- return
308
- }
309
-
310
- // Take only ONE frame worth of samples
311
- val frameData = pcmData.copyOfRange(0, samplesPerFrame)
312
-
313
- // Encode this single frame to Opus (with DRED padding if enabled)
314
- val opusData = try {
315
- encoder.encode(frameData, samplesPerFrame)
316
- } catch (e: Exception) {
317
- Log.e(TAG, "Failed to encode Opus packet: ${e.message}")
318
- frameBuffer.clear()
319
- return
320
- }
321
-
322
- if (opusData == null || opusData.isEmpty()) {
323
- Log.w(TAG, "Failed to encode Opus packet (null or empty)")
324
- frameBuffer.clear()
325
- return
326
- }
327
-
328
- // Calculate timestamp in milliseconds
329
- val timestampMs = System.currentTimeMillis().toDouble()
330
-
331
- // Emit audioChunk event with Opus packet (may be larger due to DRED)
332
- onAudioChunk?.invoke(opusData, timestampMs, sequenceNumber)
333
-
334
- sequenceNumber++
335
-
336
- // Keep any remaining samples for next packet
337
- val remainingSamples = pcmData.size - samplesPerFrame
338
- if (remainingSamples > 0) {
339
- val remaining = pcmData.copyOfRange(samplesPerFrame, pcmData.size)
340
- frameBuffer.clear()
341
- frameBuffer.add(remaining)
342
- } else {
343
- frameBuffer.clear()
344
- }
261
+ Log.d(TAG, "Capture thread stopped")
345
262
  }
346
263
  }
@@ -35,6 +35,14 @@ class OpusEncoder(
35
35
  val frameSize: Int = (sampleRate * frameSizeMs / 1000.0).toInt()
36
36
  private var encoderPtr: Long = 0
37
37
 
38
+ /**
39
+ * Encoder lookahead (pre-skip) in samples. Decoders should skip this many
40
+ * samples at the start of the stream to account for the encoder's
41
+ * algorithmic delay.
42
+ */
43
+ var preSkip: Int = 0
44
+ private set
45
+
38
46
  init {
39
47
  // Create native encoder
40
48
  encoderPtr = nativeCreate(sampleRate, channels, bitrate, dredDurationMs)
@@ -42,12 +50,16 @@ class OpusEncoder(
42
50
  throw RuntimeException("Failed to create Opus encoder")
43
51
  }
44
52
 
53
+ // Read encoder lookahead (pre-skip)
54
+ preSkip = nativeGetLookahead(encoderPtr)
55
+
45
56
  Log.i(TAG, """
46
57
  Opus encoder initialized:
47
58
  - Sample rate: ${sampleRate}Hz
48
59
  - Channels: $channels
49
60
  - Bitrate: ${bitrate / 1000}kbps
50
61
  - Frame size: $frameSize samples (${frameSizeMs}ms)
62
+ - Pre-skip: $preSkip samples
51
63
  - DRED: ${dredDurationMs}ms
52
64
  """.trimIndent())
53
65
  }
@@ -100,4 +112,6 @@ class OpusEncoder(
100
112
  ): ByteArray?
101
113
 
102
114
  private external fun nativeDestroy(encoderPtr: Long)
115
+
116
+ private external fun nativeGetLookahead(encoderPtr: Long): Int
103
117
  }
@@ -23,7 +23,7 @@ class OpuslibModule : Module() {
23
23
  Name("Opuslib")
24
24
 
25
25
  // Events
26
- Events("audioChunk", "amplitude", "error")
26
+ Events("audioChunk", "amplitude", "audioStarted", "audioEnd", "error")
27
27
 
28
28
  // Start streaming method
29
29
  AsyncFunction("startStreaming") { config: AudioConfig ->
@@ -79,13 +79,43 @@ class OpuslibModule : Module() {
79
79
  val manager = AudioRecordManager(context, config)
80
80
  android.util.Log.d(TAG, "✅ AudioRecordManager created")
81
81
 
82
- // Set up event callbacks
82
+ // Set up event callbacks — audioStarted/audioEnd come from the encoding thread
83
83
  android.util.Log.d(TAG, "🔗 Setting up event callbacks...")
84
- manager.setOnAudioChunk { data, timestamp, sequenceNumber ->
84
+ manager.setOnAudioChunk { frames, timestamp, sequenceNumber, duration, frameCount ->
85
+ // Each frame is an independent Opus packet wrapped in { data, audioLevel? }.
86
+ val frameObjects = frames.map { frame ->
87
+ val obj = mutableMapOf<String, Any>("data" to frame.data)
88
+ frame.audioLevel?.let { obj["audioLevel"] = it }
89
+ obj
90
+ }
91
+ // `data` is kept for backward compatibility: the FIRST frame's Opus packet,
92
+ // sent in the same ByteArray representation existing consumers already read.
85
93
  sendEvent("audioChunk", mapOf(
86
- "data" to data,
94
+ "data" to frames.first().data,
95
+ "frames" to frameObjects,
87
96
  "timestamp" to timestamp,
88
- "sequenceNumber" to sequenceNumber
97
+ "sequenceNumber" to sequenceNumber,
98
+ "duration" to duration,
99
+ "frameCount" to frameCount
100
+ ))
101
+ }
102
+
103
+ manager.setOnStarted { timestamp, sampleRate, channels, bitrate, frameSize, preSkip ->
104
+ sendEvent("audioStarted", mapOf(
105
+ "timestamp" to timestamp,
106
+ "sampleRate" to sampleRate,
107
+ "channels" to channels,
108
+ "bitrate" to bitrate,
109
+ "frameSize" to frameSize,
110
+ "preSkip" to preSkip
111
+ ))
112
+ }
113
+
114
+ manager.setOnEnd { timestamp, totalDuration, totalPackets ->
115
+ sendEvent("audioEnd", mapOf(
116
+ "timestamp" to timestamp,
117
+ "totalDuration" to totalDuration,
118
+ "totalPackets" to totalPackets
89
119
  ))
90
120
  }
91
121
 
@@ -121,6 +151,7 @@ class OpuslibModule : Module() {
121
151
  return
122
152
  }
123
153
 
154
+ // stop() flushes the encoding thread and emits audioEnd before tearing down.
124
155
  audioRecordManager?.stop()
125
156
  audioRecordManager = null
126
157
  isStreaming = false
@@ -169,9 +200,16 @@ class AudioConfig : Record {
169
200
  @Field
170
201
  var frameSize: Double = 20.0
171
202
 
203
+ // Legacy field, still accepted from existing callers. Superseded by
204
+ // framesPerCallback; kept so existing configs continue to deserialize.
172
205
  @Field
173
206
  var packetDuration: Double = 20.0
174
207
 
208
+ // Number of independent Opus frames batched into one audioChunk event.
209
+ // Defaults to 1 to match the previous one-frame-per-event behavior.
210
+ @Field
211
+ var framesPerCallback: Int = 1
212
+
175
213
  @Field
176
214
  var dredDuration: Int = 100 // NEW: DRED recovery duration in ms
177
215
 
@@ -181,6 +219,10 @@ class AudioConfig : Record {
181
219
  @Field
182
220
  var amplitudeEventInterval: Double = 16.0
183
221
 
222
+ // Per-frame audio level (RMS-derived, 0.0..1.0) attached to each frame.
223
+ @Field
224
+ var enableAudioLevel: Boolean = false
225
+
184
226
  @Field
185
227
  var saveDebugAudio: Boolean = false
186
228
  }
@@ -12,25 +12,90 @@ export interface AudioConfig {
12
12
  frameSize: number;
13
13
  /** Packet duration in milliseconds (typically 20-100ms) */
14
14
  packetDuration: number;
15
+ /**
16
+ * Number of independently-encoded Opus frames to batch into a single
17
+ * `audioChunk` event (default 1). Each entry in `AudioChunkEvent.frames` is a
18
+ * complete, independently decodable Opus packet — frames are never
19
+ * concatenated. Batching reduces the number of bridge calls.
20
+ */
21
+ framesPerCallback?: number;
15
22
  /** DRED recovery duration in milliseconds (0-100, default 100) - NEW in Opus 1.6 */
16
23
  dredDuration?: number;
17
24
  /** Enable amplitude events for waveform visualization */
18
25
  enableAmplitudeEvents?: boolean;
19
26
  /** Amplitude event interval in milliseconds (default 16) */
20
27
  amplitudeEventInterval?: number;
28
+ /**
29
+ * Enable per-frame audio level calculation (default false). When enabled,
30
+ * each `OpusFrame` carries an `audioLevel` (0.0 - 1.0) derived from RMS.
31
+ * Disabled by default to save computation.
32
+ */
33
+ enableAudioLevel?: boolean;
21
34
  /** Save debug PCM audio to file (development only) */
22
35
  saveDebugAudio?: boolean;
36
+ /**
37
+ * iOS AudioSession configuration (iOS only; ignored on Android and web).
38
+ * Omit to keep the default recording session
39
+ * (`record` category, `measurement` mode, no options).
40
+ */
41
+ iosAudioSession?: IOSAudioSessionConfig;
42
+ }
43
+ /**
44
+ * iOS `AVAudioSession` configuration. iOS only — ignored on Android and web.
45
+ */
46
+ export interface IOSAudioSessionConfig {
47
+ /**
48
+ * `AVAudioSession.Category`
49
+ * - `record`: pure recording (default)
50
+ * - `playAndRecord`: record and play simultaneously
51
+ * - `playback`: playback only
52
+ * - `ambient`: mix with other audio without interrupting it
53
+ */
54
+ category: 'record' | 'playAndRecord' | 'playback' | 'ambient';
55
+ /**
56
+ * `AVAudioSession.Mode`
57
+ * - `measurement`: disable system audio processing (default)
58
+ * - `default`: enable system audio processing (AGC, echo cancellation)
59
+ * - `voiceChat`: optimized for voice calls
60
+ * - `spokenAudio`: optimized for spoken content
61
+ */
62
+ mode: 'default' | 'voiceChat' | 'measurement' | 'spokenAudio';
63
+ /** `AVAudioSession.CategoryOptions` (combinable) */
64
+ options?: ('mixWithOthers' | 'defaultToSpeaker' | 'allowBluetooth' | 'allowAirPlay' | 'allowBluetoothA2DP')[];
65
+ }
66
+ /**
67
+ * A single Opus frame — one complete `opus_encode()` output with its own TOC
68
+ * byte, decodable on its own.
69
+ */
70
+ export interface OpusFrame {
71
+ /** Opus-encoded packet data (independent, decodable) */
72
+ data: ArrayBuffer;
73
+ /** Per-frame audio level (0.0 - 1.0). Present only when `enableAudioLevel` is true. */
74
+ audioLevel?: number;
23
75
  }
24
76
  /**
25
77
  * Audio chunk event payload (Opus-encoded data)
26
78
  */
27
79
  export interface AudioChunkEvent {
28
- /** Opus-encoded audio data as ArrayBuffer */
80
+ /**
81
+ * The first frame's Opus packet, kept for backward compatibility — equivalent
82
+ * to `frames[0].data`. With the default `framesPerCallback` of 1 this is the
83
+ * single packet for the event. Prefer `frames` for new code.
84
+ */
29
85
  data: ArrayBuffer;
86
+ /**
87
+ * Independently decodable Opus packets in this event (one per encoded frame).
88
+ * Contains a single entry unless `framesPerCallback` > 1.
89
+ */
90
+ frames: OpusFrame[];
30
91
  /** Timestamp in milliseconds */
31
92
  timestamp: number;
32
- /** Sequence number (increments with each packet) */
93
+ /** Sequence number (increments with each event) */
33
94
  sequenceNumber: number;
95
+ /** Duration of all frames in milliseconds (`frameSize * frameCount`) */
96
+ duration: number;
97
+ /** Number of Opus frames in this event (= `frames.length`) */
98
+ frameCount: number;
34
99
  }
35
100
  /**
36
101
  * Amplitude event payload (for waveform visualization)
@@ -43,6 +108,35 @@ export interface AmplitudeEvent {
43
108
  /** Timestamp in milliseconds */
44
109
  timestamp: number;
45
110
  }
111
+ /**
112
+ * Audio started event payload. Emitted once when streaming begins.
113
+ */
114
+ export interface AudioStartedEvent {
115
+ /** Timestamp in milliseconds when streaming started */
116
+ timestamp: number;
117
+ /** Actual sample rate in Hz */
118
+ sampleRate: number;
119
+ /** Number of channels */
120
+ channels: number;
121
+ /** Configured bitrate in bits/second */
122
+ bitrate: number;
123
+ /** Frame duration in milliseconds */
124
+ frameSize: number;
125
+ /** Opus encoder lookahead in samples — decoders should skip this many samples at the start */
126
+ preSkip: number;
127
+ }
128
+ /**
129
+ * Audio end event payload. Emitted once when streaming stops, after the final
130
+ * buffered audio has been flushed.
131
+ */
132
+ export interface AudioEndEvent {
133
+ /** Timestamp in milliseconds when streaming stopped */
134
+ timestamp: number;
135
+ /** Total session duration in milliseconds */
136
+ totalDuration: number;
137
+ /** Total number of packets (audioChunk events) emitted during the session */
138
+ totalPackets: number;
139
+ }
46
140
  /**
47
141
  * Error event payload
48
142
  */
@@ -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,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;CACvB;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;;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;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,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;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAA;IAC1B,sDAAsD;IACtD,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB;;;;OAIG;IACH,eAAe,CAAC,EAAE,qBAAqB,CAAA;CACxC;AAED;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC;;;;;;OAMG;IACH,QAAQ,EAAE,QAAQ,GAAG,eAAe,GAAG,UAAU,GAAG,SAAS,CAAA;IAC7D;;;;;;OAMG;IACH,IAAI,EAAE,SAAS,GAAG,WAAW,GAAG,aAAa,GAAG,aAAa,CAAA;IAC7D,oDAAoD;IACpD,OAAO,CAAC,EAAE,CACN,eAAe,GACf,kBAAkB,GAClB,gBAAgB,GAChB,cAAc,GACd,oBAAoB,CACvB,EAAE,CAAA;CACJ;AAED;;;GAGG;AACH,MAAM,WAAW,SAAS;IACxB,wDAAwD;IACxD,IAAI,EAAE,WAAW,CAAA;IACjB,uFAAuF;IACvF,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B;;;;OAIG;IACH,IAAI,EAAE,WAAW,CAAA;IACjB;;;OAGG;IACH,MAAM,EAAE,SAAS,EAAE,CAAA;IACnB,gCAAgC;IAChC,SAAS,EAAE,MAAM,CAAA;IACjB,mDAAmD;IACnD,cAAc,EAAE,MAAM,CAAA;IACtB,wEAAwE;IACxE,QAAQ,EAAE,MAAM,CAAA;IAChB,8DAA8D;IAC9D,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;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,uDAAuD;IACvD,SAAS,EAAE,MAAM,CAAA;IACjB,+BAA+B;IAC/B,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,8FAA8F;IAC9F,OAAO,EAAE,MAAM,CAAA;CAChB;AAED;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,uDAAuD;IACvD,SAAS,EAAE,MAAM,CAAA;IACjB,6CAA6C;IAC7C,aAAa,EAAE,MAAM,CAAA;IACrB,6EAA6E;IAC7E,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 /** 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}\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 * 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 /**\n * Number of independently-encoded Opus frames to batch into a single\n * `audioChunk` event (default 1). Each entry in `AudioChunkEvent.frames` is a\n * complete, independently decodable Opus packet — frames are never\n * concatenated. Batching reduces the number of bridge calls.\n */\n framesPerCallback?: 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 /**\n * Enable per-frame audio level calculation (default false). When enabled,\n * each `OpusFrame` carries an `audioLevel` (0.0 - 1.0) derived from RMS.\n * Disabled by default to save computation.\n */\n enableAudioLevel?: boolean\n /** Save debug PCM audio to file (development only) */\n saveDebugAudio?: boolean\n /**\n * iOS AudioSession configuration (iOS only; ignored on Android and web).\n * Omit to keep the default recording session\n * (`record` category, `measurement` mode, no options).\n */\n iosAudioSession?: IOSAudioSessionConfig\n}\n\n/**\n * iOS `AVAudioSession` configuration. iOS only — ignored on Android and web.\n */\nexport interface IOSAudioSessionConfig {\n /**\n * `AVAudioSession.Category`\n * - `record`: pure recording (default)\n * - `playAndRecord`: record and play simultaneously\n * - `playback`: playback only\n * - `ambient`: mix with other audio without interrupting it\n */\n category: 'record' | 'playAndRecord' | 'playback' | 'ambient'\n /**\n * `AVAudioSession.Mode`\n * - `measurement`: disable system audio processing (default)\n * - `default`: enable system audio processing (AGC, echo cancellation)\n * - `voiceChat`: optimized for voice calls\n * - `spokenAudio`: optimized for spoken content\n */\n mode: 'default' | 'voiceChat' | 'measurement' | 'spokenAudio'\n /** `AVAudioSession.CategoryOptions` (combinable) */\n options?: (\n | 'mixWithOthers'\n | 'defaultToSpeaker'\n | 'allowBluetooth'\n | 'allowAirPlay'\n | 'allowBluetoothA2DP'\n )[]\n}\n\n/**\n * A single Opus frame — one complete `opus_encode()` output with its own TOC\n * byte, decodable on its own.\n */\nexport interface OpusFrame {\n /** Opus-encoded packet data (independent, decodable) */\n data: ArrayBuffer\n /** Per-frame audio level (0.0 - 1.0). Present only when `enableAudioLevel` is true. */\n audioLevel?: number\n}\n\n/**\n * Audio chunk event payload (Opus-encoded data)\n */\nexport interface AudioChunkEvent {\n /**\n * The first frame's Opus packet, kept for backward compatibility — equivalent\n * to `frames[0].data`. With the default `framesPerCallback` of 1 this is the\n * single packet for the event. Prefer `frames` for new code.\n */\n data: ArrayBuffer\n /**\n * Independently decodable Opus packets in this event (one per encoded frame).\n * Contains a single entry unless `framesPerCallback` > 1.\n */\n frames: OpusFrame[]\n /** Timestamp in milliseconds */\n timestamp: number\n /** Sequence number (increments with each event) */\n sequenceNumber: number\n /** Duration of all frames in milliseconds (`frameSize * frameCount`) */\n duration: number\n /** Number of Opus frames in this event (= `frames.length`) */\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. Emitted once when streaming begins.\n */\nexport interface AudioStartedEvent {\n /** Timestamp in milliseconds when streaming started */\n timestamp: number\n /** Actual sample rate in Hz */\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 — decoders should skip this many samples at the start */\n preSkip: number\n}\n\n/**\n * Audio end event payload. Emitted once when streaming stops, after the final\n * buffered audio has been flushed.\n */\nexport interface AudioEndEvent {\n /** Timestamp in milliseconds when streaming stopped */\n timestamp: number\n /** Total session duration in milliseconds */\n totalDuration: number\n /** Total number of packets (audioChunk events) emitted 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,4 +1,4 @@
1
- import type { AudioConfig, AudioChunkEvent, AmplitudeEvent, ErrorEvent, Subscription } from './Opuslib.types';
1
+ import type { AudioConfig, AudioChunkEvent, AmplitudeEvent, AudioStartedEvent, AudioEndEvent, ErrorEvent, Subscription } from './Opuslib.types';
2
2
  /**
3
3
  * Opuslib - Opus 1.6.1 Audio Encoding with DRED Support
4
4
  *
@@ -49,6 +49,14 @@ declare const _default: {
49
49
  * websocket.send(event.data)
50
50
  * })
51
51
  *
52
+ * // Listen for session lifecycle
53
+ * Opuslib.addListener('audioStarted', (event) => {
54
+ * console.log('Started; decoder pre-skip:', event.preSkip)
55
+ * })
56
+ * Opuslib.addListener('audioEnd', (event) => {
57
+ * console.log(`Ended: ${event.totalPackets} packets in ${event.totalDuration}ms`)
58
+ * })
59
+ *
52
60
  * // Listen for errors
53
61
  * const errorSub = Opuslib.addListener('error', (event) => {
54
62
  * console.error('Error:', event.message)
@@ -62,6 +70,8 @@ declare const _default: {
62
70
  addListener: {
63
71
  (eventName: "audioChunk", listener: (event: AudioChunkEvent) => void): Subscription;
64
72
  (eventName: "amplitude", listener: (event: AmplitudeEvent) => void): Subscription;
73
+ (eventName: "audioStarted", listener: (event: AudioStartedEvent) => void): Subscription;
74
+ (eventName: "audioEnd", listener: (event: AudioEndEvent) => void): Subscription;
65
75
  (eventName: "error", listener: (event: ErrorEvent) => void): Subscription;
66
76
  };
67
77
  /**
@@ -71,6 +81,23 @@ declare const _default: {
71
81
  * @returns Subscription object with remove() method
72
82
  */
73
83
  addAmplitudeListener: (listener: (event: AmplitudeEvent) => void) => Subscription;
84
+ /**
85
+ * Listen for the `audioStarted` event, emitted once when streaming begins.
86
+ * Carries the active config and the Opus encoder `preSkip` (lookahead) so a
87
+ * decoder knows how many samples to skip at the start of the stream.
88
+ *
89
+ * @param listener Event listener callback
90
+ * @returns Subscription object with remove() method
91
+ */
92
+ addAudioStartedListener: (listener: (event: AudioStartedEvent) => void) => Subscription;
93
+ /**
94
+ * Listen for the `audioEnd` event, emitted once when streaming stops (after
95
+ * the final buffered audio has been flushed). Carries the session summary.
96
+ *
97
+ * @param listener Event listener callback
98
+ * @returns Subscription object with remove() method
99
+ */
100
+ addAudioEndListener: (listener: (event: AudioEndEvent) => void) => Subscription;
74
101
  /**
75
102
  * Listen for error events
76
103
  *