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.
@@ -1 +1 @@
1
- {"version":3,"file":"OpuslibModule.d.ts","sourceRoot":"","sources":["../src/OpuslibModule.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,WAAW,EACX,eAAe,EACf,cAAc,EACd,UAAU,EACV,YAAY,EACb,MAAM,iBAAiB,CAAA;AAoCxB;;;;;GAKG;;IAED;;;;;;;;;;;;;;;OAeG;6BACsB,WAAW;IAEpC;;OAEG;;IAGH;;OAEG;;IAGH;;OAEG;;IAGH;;;;;;;;;;;;;;;;;;;;;;;OAuBG;iBAIsE;QACvE,CACE,SAAS,EAAE,YAAY,EACvB,QAAQ,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,GACzC,YAAY,CAAA;QACf,CACE,SAAS,EAAE,WAAW,EACtB,QAAQ,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,GACxC,YAAY,CAAA;QACf,CAAC,SAAS,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,GAAG,YAAY,CAAA;KAC1E;IAED;;;;;OAKG;qCAES,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,KACxC,YAAY;IAEf;;;;;OAKG;iCAC0B,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,KAAG,YAAY;;AAzFzE,wBA2FC"}
1
+ {"version":3,"file":"OpuslibModule.d.ts","sourceRoot":"","sources":["../src/OpuslibModule.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,WAAW,EACX,eAAe,EACf,cAAc,EACd,iBAAiB,EACjB,aAAa,EACb,UAAU,EACV,YAAY,EACb,MAAM,iBAAiB,CAAA;AAoCxB;;;;;GAKG;;IAED;;;;;;;;;;;;;;;OAeG;6BACsB,WAAW;IAEpC;;OAEG;;IAGH;;OAEG;;IAGH;;OAEG;;IAGH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA+BG;iBAgBsE;QACvE,CACE,SAAS,EAAE,YAAY,EACvB,QAAQ,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,GACzC,YAAY,CAAA;QACf,CACE,SAAS,EAAE,WAAW,EACtB,QAAQ,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,GACxC,YAAY,CAAA;QACf,CACE,SAAS,EAAE,cAAc,EACzB,QAAQ,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,GAC3C,YAAY,CAAA;QACf,CACE,SAAS,EAAE,UAAU,EACrB,QAAQ,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,GACvC,YAAY,CAAA;QACf,CAAC,SAAS,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,GAAG,YAAY,CAAA;KAC1E;IAED;;;;;OAKG;qCAES,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,KACxC,YAAY;IAEf;;;;;;;OAOG;wCAES,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,KAC3C,YAAY;IAEf;;;;;;OAMG;oCAES,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,KACvC,YAAY;IAEf;;;;;OAKG;iCAC0B,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,KAAG,YAAY;;AA5IzE,wBA8IC"}
@@ -53,6 +53,14 @@ export default {
53
53
  * websocket.send(event.data)
54
54
  * })
55
55
  *
56
+ * // Listen for session lifecycle
57
+ * Opuslib.addListener('audioStarted', (event) => {
58
+ * console.log('Started; decoder pre-skip:', event.preSkip)
59
+ * })
60
+ * Opuslib.addListener('audioEnd', (event) => {
61
+ * console.log(`Ended: ${event.totalPackets} packets in ${event.totalDuration}ms`)
62
+ * })
63
+ *
56
64
  * // Listen for errors
57
65
  * const errorSub = Opuslib.addListener('error', (event) => {
58
66
  * console.error('Error:', event.message)
@@ -71,6 +79,23 @@ export default {
71
79
  * @returns Subscription object with remove() method
72
80
  */
73
81
  addAmplitudeListener: (listener) => emitter.addListener('amplitude', listener),
82
+ /**
83
+ * Listen for the `audioStarted` event, emitted once when streaming begins.
84
+ * Carries the active config and the Opus encoder `preSkip` (lookahead) so a
85
+ * decoder knows how many samples to skip at the start of the stream.
86
+ *
87
+ * @param listener Event listener callback
88
+ * @returns Subscription object with remove() method
89
+ */
90
+ addAudioStartedListener: (listener) => emitter.addListener('audioStarted', listener),
91
+ /**
92
+ * Listen for the `audioEnd` event, emitted once when streaming stops (after
93
+ * the final buffered audio has been flushed). Carries the session summary.
94
+ *
95
+ * @param listener Event listener callback
96
+ * @returns Subscription object with remove() method
97
+ */
98
+ addAudioEndListener: (listener) => emitter.addListener('audioEnd', listener),
74
99
  /**
75
100
  * Listen for error events
76
101
  *
@@ -1 +1 @@
1
- {"version":3,"file":"OpuslibModule.js","sourceRoot":"","sources":["../src/OpuslibModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,mBAAmB,EAAE,YAAY,EAAE,MAAM,MAAM,CAAA;AAsCtE,kCAAkC;AAClC,MAAM,aAAa,GAAG,mBAAmB,CAAoB,SAAS,CAAC,CAAA;AAEvE,+CAA+C;AAC/C,MAAM,OAAO,GAAG,IAAI,YAAY,CAAC,aAAoB,CAAC,CAAA;AAEtD;;;;;GAKG;AACH,eAAe;IACb;;;;;;;;;;;;;;;OAeG;IACH,cAAc,EAAE,CAAC,MAAmB,EAAE,EAAE,CAAC,aAAa,CAAC,cAAc,CAAC,MAAM,CAAC;IAE7E;;OAEG;IACH,aAAa,EAAE,GAAG,EAAE,CAAC,aAAa,CAAC,aAAa,EAAE;IAElD;;OAEG;IACH,cAAc,EAAE,GAAG,EAAE,CAAC,aAAa,CAAC,cAAc,EAAE;IAEpD;;OAEG;IACH,eAAe,EAAE,GAAG,EAAE,CAAC,aAAa,CAAC,eAAe,EAAE;IAEtD;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACH,WAAW,EAAE,CAAC,CACZ,SAA+C,EAC/C,QAAwE,EAC1D,EAAE,CAAE,OAAe,CAAC,WAAW,CAAC,SAAS,EAAE,QAAQ,CAAC,CAUnE;IAED;;;;;OAKG;IACH,oBAAoB,EAAE,CACpB,QAAyC,EAC3B,EAAE,CAAE,OAAe,CAAC,WAAW,CAAC,WAAW,EAAE,QAAQ,CAAC;IAEtE;;;;;OAKG;IACH,gBAAgB,EAAE,CAAC,QAAqC,EAAgB,EAAE,CACvE,OAAe,CAAC,WAAW,CAAC,OAAO,EAAE,QAAQ,CAAC;CAClD,CAAA","sourcesContent":["import { NativeModule, requireNativeModule, EventEmitter } from 'expo'\n\nimport type {\n AudioConfig,\n AudioChunkEvent,\n AmplitudeEvent,\n ErrorEvent,\n Subscription,\n} from './Opuslib.types'\n\n/**\n * Opuslib Native Module Interface\n *\n * Provides native audio capture and Opus 1.6.1 encoding with DRED support\n */\ndeclare class OpuslibModuleType extends NativeModule {\n /**\n * Start audio streaming with Opus encoding\n * @param config Audio configuration\n */\n startStreaming(config: AudioConfig): Promise<void>\n\n /**\n * Stop audio streaming\n */\n stopStreaming(): Promise<void>\n\n /**\n * Pause audio streaming (keeps resources allocated)\n */\n pauseStreaming(): void\n\n /**\n * Resume audio streaming\n */\n resumeStreaming(): void\n}\n\n// Load the native module from JSI\nconst OpuslibModule = requireNativeModule<OpuslibModuleType>('Opuslib')\n\n// Create event emitter for listening to events\nconst emitter = new EventEmitter(OpuslibModule as any)\n\n/**\n * Opuslib - Opus 1.6.1 Audio Encoding with DRED Support\n *\n * This module provides real-time audio capture and Opus 1.6.1 encoding\n * with Deep Redundancy (DRED) for improved quality on lossy networks.\n */\nexport default {\n /**\n * Start audio streaming with Opus encoding\n *\n * @param config Audio configuration\n * @example\n * ```ts\n * await Opuslib.startStreaming({\n * sampleRate: 16000,\n * channels: 1,\n * bitrate: 24000,\n * frameSize: 20,\n * packetDuration: 20,\n * dredDuration: 100, // Enable 100ms DRED recovery\n * })\n * ```\n */\n startStreaming: (config: AudioConfig) => OpuslibModule.startStreaming(config),\n\n /**\n * Stop audio streaming and release resources\n */\n stopStreaming: () => OpuslibModule.stopStreaming(),\n\n /**\n * Pause audio streaming (keeps resources allocated)\n */\n pauseStreaming: () => OpuslibModule.pauseStreaming(),\n\n /**\n * Resume audio streaming\n */\n resumeStreaming: () => OpuslibModule.resumeStreaming(),\n\n /**\n * Listen for events (audioChunk, amplitude, or error)\n *\n * @param eventName Event type to listen for\n * @param listener Event listener callback\n * @returns Subscription object with remove() method\n * @example\n * ```ts\n * // Listen for audio chunks\n * const subscription = Opuslib.addListener('audioChunk', (event) => {\n * console.log('Received Opus packet:', event.data.byteLength, 'bytes')\n * websocket.send(event.data)\n * })\n *\n * // Listen for errors\n * const errorSub = Opuslib.addListener('error', (event) => {\n * console.error('Error:', event.message)\n * })\n *\n * // Later: unsubscribe\n * subscription.remove()\n * errorSub.remove()\n * ```\n */\n addListener: ((\n eventName: 'audioChunk' | 'amplitude' | 'error',\n listener: (event: AudioChunkEvent | AmplitudeEvent | ErrorEvent) => void,\n ): Subscription => (emitter as any).addListener(eventName, listener)) as {\n (\n eventName: 'audioChunk',\n listener: (event: AudioChunkEvent) => void,\n ): Subscription\n (\n eventName: 'amplitude',\n listener: (event: AmplitudeEvent) => void,\n ): Subscription\n (eventName: 'error', listener: (event: ErrorEvent) => void): Subscription\n },\n\n /**\n * Listen for amplitude events (for waveform visualization)\n *\n * @param listener Event listener callback\n * @returns Subscription object with remove() method\n */\n addAmplitudeListener: (\n listener: (event: AmplitudeEvent) => void,\n ): Subscription => (emitter as any).addListener('amplitude', listener),\n\n /**\n * Listen for error events\n *\n * @param listener Event listener callback\n * @returns Subscription object with remove() method\n */\n addErrorListener: (listener: (event: ErrorEvent) => void): Subscription =>\n (emitter as any).addListener('error', listener),\n}\n"]}
1
+ {"version":3,"file":"OpuslibModule.js","sourceRoot":"","sources":["../src/OpuslibModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,mBAAmB,EAAE,YAAY,EAAE,MAAM,MAAM,CAAA;AAwCtE,kCAAkC;AAClC,MAAM,aAAa,GAAG,mBAAmB,CAAoB,SAAS,CAAC,CAAA;AAEvE,+CAA+C;AAC/C,MAAM,OAAO,GAAG,IAAI,YAAY,CAAC,aAAoB,CAAC,CAAA;AAEtD;;;;;GAKG;AACH,eAAe;IACb;;;;;;;;;;;;;;;OAeG;IACH,cAAc,EAAE,CAAC,MAAmB,EAAE,EAAE,CAAC,aAAa,CAAC,cAAc,CAAC,MAAM,CAAC;IAE7E;;OAEG;IACH,aAAa,EAAE,GAAG,EAAE,CAAC,aAAa,CAAC,aAAa,EAAE;IAElD;;OAEG;IACH,cAAc,EAAE,GAAG,EAAE,CAAC,aAAa,CAAC,cAAc,EAAE;IAEpD;;OAEG;IACH,eAAe,EAAE,GAAG,EAAE,CAAC,aAAa,CAAC,eAAe,EAAE;IAEtD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA+BG;IACH,WAAW,EAAE,CAAC,CACZ,SAKW,EACX,QAOS,EACK,EAAE,CAAE,OAAe,CAAC,WAAW,CAAC,SAAS,EAAE,QAAQ,CAAC,CAkBnE;IAED;;;;;OAKG;IACH,oBAAoB,EAAE,CACpB,QAAyC,EAC3B,EAAE,CAAE,OAAe,CAAC,WAAW,CAAC,WAAW,EAAE,QAAQ,CAAC;IAEtE;;;;;;;OAOG;IACH,uBAAuB,EAAE,CACvB,QAA4C,EAC9B,EAAE,CAAE,OAAe,CAAC,WAAW,CAAC,cAAc,EAAE,QAAQ,CAAC;IAEzE;;;;;;OAMG;IACH,mBAAmB,EAAE,CACnB,QAAwC,EAC1B,EAAE,CAAE,OAAe,CAAC,WAAW,CAAC,UAAU,EAAE,QAAQ,CAAC;IAErE;;;;;OAKG;IACH,gBAAgB,EAAE,CAAC,QAAqC,EAAgB,EAAE,CACvE,OAAe,CAAC,WAAW,CAAC,OAAO,EAAE,QAAQ,CAAC;CAClD,CAAA","sourcesContent":["import { NativeModule, requireNativeModule, EventEmitter } from 'expo'\n\nimport type {\n AudioConfig,\n AudioChunkEvent,\n AmplitudeEvent,\n AudioStartedEvent,\n AudioEndEvent,\n ErrorEvent,\n Subscription,\n} from './Opuslib.types'\n\n/**\n * Opuslib Native Module Interface\n *\n * Provides native audio capture and Opus 1.6.1 encoding with DRED support\n */\ndeclare class OpuslibModuleType extends NativeModule {\n /**\n * Start audio streaming with Opus encoding\n * @param config Audio configuration\n */\n startStreaming(config: AudioConfig): Promise<void>\n\n /**\n * Stop audio streaming\n */\n stopStreaming(): Promise<void>\n\n /**\n * Pause audio streaming (keeps resources allocated)\n */\n pauseStreaming(): void\n\n /**\n * Resume audio streaming\n */\n resumeStreaming(): void\n}\n\n// Load the native module from JSI\nconst OpuslibModule = requireNativeModule<OpuslibModuleType>('Opuslib')\n\n// Create event emitter for listening to events\nconst emitter = new EventEmitter(OpuslibModule as any)\n\n/**\n * Opuslib - Opus 1.6.1 Audio Encoding with DRED Support\n *\n * This module provides real-time audio capture and Opus 1.6.1 encoding\n * with Deep Redundancy (DRED) for improved quality on lossy networks.\n */\nexport default {\n /**\n * Start audio streaming with Opus encoding\n *\n * @param config Audio configuration\n * @example\n * ```ts\n * await Opuslib.startStreaming({\n * sampleRate: 16000,\n * channels: 1,\n * bitrate: 24000,\n * frameSize: 20,\n * packetDuration: 20,\n * dredDuration: 100, // Enable 100ms DRED recovery\n * })\n * ```\n */\n startStreaming: (config: AudioConfig) => OpuslibModule.startStreaming(config),\n\n /**\n * Stop audio streaming and release resources\n */\n stopStreaming: () => OpuslibModule.stopStreaming(),\n\n /**\n * Pause audio streaming (keeps resources allocated)\n */\n pauseStreaming: () => OpuslibModule.pauseStreaming(),\n\n /**\n * Resume audio streaming\n */\n resumeStreaming: () => OpuslibModule.resumeStreaming(),\n\n /**\n * Listen for events (audioChunk, amplitude, or error)\n *\n * @param eventName Event type to listen for\n * @param listener Event listener callback\n * @returns Subscription object with remove() method\n * @example\n * ```ts\n * // Listen for audio chunks\n * const subscription = Opuslib.addListener('audioChunk', (event) => {\n * console.log('Received Opus packet:', event.data.byteLength, 'bytes')\n * websocket.send(event.data)\n * })\n *\n * // Listen for session lifecycle\n * Opuslib.addListener('audioStarted', (event) => {\n * console.log('Started; decoder pre-skip:', event.preSkip)\n * })\n * Opuslib.addListener('audioEnd', (event) => {\n * console.log(`Ended: ${event.totalPackets} packets in ${event.totalDuration}ms`)\n * })\n *\n * // Listen for errors\n * const errorSub = Opuslib.addListener('error', (event) => {\n * console.error('Error:', event.message)\n * })\n *\n * // Later: unsubscribe\n * subscription.remove()\n * errorSub.remove()\n * ```\n */\n addListener: ((\n eventName:\n | 'audioChunk'\n | 'amplitude'\n | 'audioStarted'\n | 'audioEnd'\n | 'error',\n listener: (\n event:\n | AudioChunkEvent\n | AmplitudeEvent\n | AudioStartedEvent\n | AudioEndEvent\n | ErrorEvent,\n ) => void,\n ): Subscription => (emitter as any).addListener(eventName, listener)) as {\n (\n eventName: 'audioChunk',\n listener: (event: AudioChunkEvent) => void,\n ): Subscription\n (\n eventName: 'amplitude',\n listener: (event: AmplitudeEvent) => void,\n ): Subscription\n (\n eventName: 'audioStarted',\n listener: (event: AudioStartedEvent) => void,\n ): Subscription\n (\n eventName: 'audioEnd',\n listener: (event: AudioEndEvent) => void,\n ): Subscription\n (eventName: 'error', listener: (event: ErrorEvent) => void): Subscription\n },\n\n /**\n * Listen for amplitude events (for waveform visualization)\n *\n * @param listener Event listener callback\n * @returns Subscription object with remove() method\n */\n addAmplitudeListener: (\n listener: (event: AmplitudeEvent) => void,\n ): Subscription => (emitter as any).addListener('amplitude', listener),\n\n /**\n * Listen for the `audioStarted` event, emitted once when streaming begins.\n * Carries the active config and the Opus encoder `preSkip` (lookahead) so a\n * decoder knows how many samples to skip at the start of the stream.\n *\n * @param listener Event listener callback\n * @returns Subscription object with remove() method\n */\n addAudioStartedListener: (\n listener: (event: AudioStartedEvent) => void,\n ): Subscription => (emitter as any).addListener('audioStarted', listener),\n\n /**\n * Listen for the `audioEnd` event, emitted once when streaming stops (after\n * the final buffered audio has been flushed). Carries the session summary.\n *\n * @param listener Event listener callback\n * @returns Subscription object with remove() method\n */\n addAudioEndListener: (\n listener: (event: AudioEndEvent) => void,\n ): Subscription => (emitter as any).addListener('audioEnd', listener),\n\n /**\n * Listen for error events\n *\n * @param listener Event listener callback\n * @returns Subscription object with remove() method\n */\n addErrorListener: (listener: (event: ErrorEvent) => void): Subscription =>\n (emitter as any).addListener('error', listener),\n}\n"]}
@@ -16,6 +16,12 @@ declare const _default: {
16
16
  addAmplitudeListener: () => {
17
17
  remove: () => void;
18
18
  };
19
+ addAudioStartedListener: () => {
20
+ remove: () => void;
21
+ };
22
+ addAudioEndListener: () => {
23
+ remove: () => void;
24
+ };
19
25
  addErrorListener: () => {
20
26
  remove: () => void;
21
27
  };
@@ -1 +1 @@
1
- {"version":3,"file":"OpuslibModule.web.d.ts","sourceRoot":"","sources":["../src/OpuslibModule.web.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA;AAElD;;;;;GAKG;;6BAE8B,WAAW,KAAG,OAAO,CAAC,IAAI,CAAC;yBAMjC,OAAO,CAAC,IAAI,CAAC;0BAMlB,IAAI;2BAMH,IAAI;;;;;;;;;;;AAnB3B,wBAoCC"}
1
+ {"version":3,"file":"OpuslibModule.web.d.ts","sourceRoot":"","sources":["../src/OpuslibModule.web.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA;AAElD;;;;;GAKG;;6BAE8B,WAAW,KAAG,OAAO,CAAC,IAAI,CAAC;yBAMjC,OAAO,CAAC,IAAI,CAAC;0BAMlB,IAAI;2BAMH,IAAI;;;;;;;;;;;;;;;;;AAnB3B,wBA4CC"}
@@ -23,6 +23,12 @@ export default {
23
23
  addAmplitudeListener: () => ({
24
24
  remove: () => { },
25
25
  }),
26
+ addAudioStartedListener: () => ({
27
+ remove: () => { },
28
+ }),
29
+ addAudioEndListener: () => ({
30
+ remove: () => { },
31
+ }),
26
32
  addErrorListener: () => ({
27
33
  remove: () => { },
28
34
  }),
@@ -1 +1 @@
1
- {"version":3,"file":"OpuslibModule.web.js","sourceRoot":"","sources":["../src/OpuslibModule.web.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,eAAe;IACb,cAAc,EAAE,KAAK,EAAE,MAAmB,EAAiB,EAAE;QAC3D,MAAM,IAAI,KAAK,CACb,+DAA+D,CAChE,CAAA;IACH,CAAC;IAED,aAAa,EAAE,KAAK,IAAmB,EAAE;QACvC,MAAM,IAAI,KAAK,CACb,+DAA+D,CAChE,CAAA;IACH,CAAC;IAED,cAAc,EAAE,GAAS,EAAE;QACzB,MAAM,IAAI,KAAK,CACb,+DAA+D,CAChE,CAAA;IACH,CAAC;IAED,eAAe,EAAE,GAAS,EAAE;QAC1B,MAAM,IAAI,KAAK,CACb,+DAA+D,CAChE,CAAA;IACH,CAAC;IAED,WAAW,EAAE,GAAG,EAAE,CAAC,CAAC;QAClB,MAAM,EAAE,GAAG,EAAE,GAAE,CAAC;KACjB,CAAC;IAEF,oBAAoB,EAAE,GAAG,EAAE,CAAC,CAAC;QAC3B,MAAM,EAAE,GAAG,EAAE,GAAE,CAAC;KACjB,CAAC;IAEF,gBAAgB,EAAE,GAAG,EAAE,CAAC,CAAC;QACvB,MAAM,EAAE,GAAG,EAAE,GAAE,CAAC;KACjB,CAAC;CACH,CAAA","sourcesContent":["import type { AudioConfig } from './Opuslib.types'\n\n/**\n * Web implementation of Opuslib (placeholder)\n *\n * Note: Web implementation requires WebRTC or WebAssembly Opus encoder\n * This is a stub for now - native platforms only\n */\nexport default {\n startStreaming: async (config: AudioConfig): Promise<void> => {\n throw new Error(\n 'Opuslib is not supported on web platform. Use iOS or Android.',\n )\n },\n\n stopStreaming: async (): Promise<void> => {\n throw new Error(\n 'Opuslib is not supported on web platform. Use iOS or Android.',\n )\n },\n\n pauseStreaming: (): void => {\n throw new Error(\n 'Opuslib is not supported on web platform. Use iOS or Android.',\n )\n },\n\n resumeStreaming: (): void => {\n throw new Error(\n 'Opuslib is not supported on web platform. Use iOS or Android.',\n )\n },\n\n addListener: () => ({\n remove: () => {},\n }),\n\n addAmplitudeListener: () => ({\n remove: () => {},\n }),\n\n addErrorListener: () => ({\n remove: () => {},\n }),\n}\n"]}
1
+ {"version":3,"file":"OpuslibModule.web.js","sourceRoot":"","sources":["../src/OpuslibModule.web.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,eAAe;IACb,cAAc,EAAE,KAAK,EAAE,MAAmB,EAAiB,EAAE;QAC3D,MAAM,IAAI,KAAK,CACb,+DAA+D,CAChE,CAAA;IACH,CAAC;IAED,aAAa,EAAE,KAAK,IAAmB,EAAE;QACvC,MAAM,IAAI,KAAK,CACb,+DAA+D,CAChE,CAAA;IACH,CAAC;IAED,cAAc,EAAE,GAAS,EAAE;QACzB,MAAM,IAAI,KAAK,CACb,+DAA+D,CAChE,CAAA;IACH,CAAC;IAED,eAAe,EAAE,GAAS,EAAE;QAC1B,MAAM,IAAI,KAAK,CACb,+DAA+D,CAChE,CAAA;IACH,CAAC;IAED,WAAW,EAAE,GAAG,EAAE,CAAC,CAAC;QAClB,MAAM,EAAE,GAAG,EAAE,GAAE,CAAC;KACjB,CAAC;IAEF,oBAAoB,EAAE,GAAG,EAAE,CAAC,CAAC;QAC3B,MAAM,EAAE,GAAG,EAAE,GAAE,CAAC;KACjB,CAAC;IAEF,uBAAuB,EAAE,GAAG,EAAE,CAAC,CAAC;QAC9B,MAAM,EAAE,GAAG,EAAE,GAAE,CAAC;KACjB,CAAC;IAEF,mBAAmB,EAAE,GAAG,EAAE,CAAC,CAAC;QAC1B,MAAM,EAAE,GAAG,EAAE,GAAE,CAAC;KACjB,CAAC;IAEF,gBAAgB,EAAE,GAAG,EAAE,CAAC,CAAC;QACvB,MAAM,EAAE,GAAG,EAAE,GAAE,CAAC;KACjB,CAAC;CACH,CAAA","sourcesContent":["import type { AudioConfig } from './Opuslib.types'\n\n/**\n * Web implementation of Opuslib (placeholder)\n *\n * Note: Web implementation requires WebRTC or WebAssembly Opus encoder\n * This is a stub for now - native platforms only\n */\nexport default {\n startStreaming: async (config: AudioConfig): Promise<void> => {\n throw new Error(\n 'Opuslib is not supported on web platform. Use iOS or Android.',\n )\n },\n\n stopStreaming: async (): Promise<void> => {\n throw new Error(\n 'Opuslib is not supported on web platform. Use iOS or Android.',\n )\n },\n\n pauseStreaming: (): void => {\n throw new Error(\n 'Opuslib is not supported on web platform. Use iOS or Android.',\n )\n },\n\n resumeStreaming: (): void => {\n throw new Error(\n 'Opuslib is not supported on web platform. Use iOS or Android.',\n )\n },\n\n addListener: () => ({\n remove: () => {},\n }),\n\n addAmplitudeListener: () => ({\n remove: () => {},\n }),\n\n addAudioStartedListener: () => ({\n remove: () => {},\n }),\n\n addAudioEndListener: () => ({\n remove: () => {},\n }),\n\n addErrorListener: () => ({\n remove: () => {},\n }),\n}\n"]}
@@ -2,52 +2,42 @@ import AVFoundation
2
2
  import ExpoModulesCore
3
3
 
4
4
  /**
5
- * AudioEngineManager - Manages AVAudioEngine for real-time audio capture with Opus 1.6.1 DRED
5
+ * AudioEngineManager - Manages AVAudioEngine for real-time audio capture.
6
6
  *
7
- * This class handles:
8
- * - Audio session configuration
9
- * - AVAudioEngine setup and lifecycle
10
- * - Real-time PCM audio capture
11
- * - Opus 1.6.1 encoding with DRED support
12
- * - Audio interruption handling
13
- * - Microphone permission management
7
+ * This class handles ONLY audio capture (AVAudioEngine lifecycle, audio session
8
+ * configuration, the real-time tap callback, and interruption handling). All
9
+ * Opus 1.6.1 encoding is delegated to AudioProcessor, which runs on its own
10
+ * serial queue. The tap callback only converts the PCM format and copies the
11
+ * samples across to the encoding queue it never blocks on the encoder.
14
12
  */
15
13
  class AudioEngineManager {
16
14
  // Audio engine and nodes
17
15
  private var audioEngine: AVAudioEngine?
18
16
  private var inputNode: AVAudioInputNode?
19
17
 
20
- // Opus encoder
21
- private var opusEncoder: OpusEncoder?
22
-
23
18
  // Audio format converter
24
19
  private var audioConverter: AVAudioConverter?
25
20
 
26
- // Configuration
27
- private var config: AudioConfig
28
- private var sequenceNumber: Int = 0
21
+ // Encoding processor (owns the encoder, runs on a separate serial queue)
22
+ private var processor: AudioProcessor?
23
+
24
+ // Configuration (immutable after init)
25
+ private let config: AudioConfig
29
26
 
30
- // State
27
+ // Recording state
31
28
  private var isRecording = false
32
29
  private var isPaused = false
33
30
  private var loggedFirstBuffer = false
34
31
 
35
- // Frame accumulation for packet duration
36
- private var frameBuffer: [[Int16]] = []
37
- private let framesPerPacket: Int
38
-
39
32
  // Event callbacks
40
- private var onAudioChunk: ((Data, Double, Int) -> Void)?
33
+ private var onAudioChunk: (([EncodedFrame], Double, Int, Double, Int) -> Void)?
34
+ private var onStarted: ((_ timestamp: Double, _ sampleRate: Int, _ channels: Int, _ bitrate: Int, _ frameSize: Double, _ preSkip: Int) -> Void)?
35
+ private var onEnd: ((_ timestamp: Double, _ totalDuration: Double, _ totalPackets: Int) -> Void)?
41
36
  private var onAmplitude: ((Float, Float, Double) -> Void)?
42
37
  private var onError: ((Error) -> Void)?
43
38
 
44
- // Debug file handles
45
- private var pcmFileHandle: FileHandle?
46
- private var pcmFileURL: URL?
47
-
48
39
  init(config: AudioConfig) {
49
40
  self.config = config
50
- self.framesPerPacket = Int(config.packetDuration / config.frameSize)
51
41
 
52
42
  // Register for interruption notifications
53
43
  NotificationCenter.default.addObserver(
@@ -68,15 +58,28 @@ class AudioEngineManager {
68
58
  // Configure audio session
69
59
  try configureAudioSession()
70
60
 
71
- // Create Opus encoder with DRED support
72
- let dredDuration = config.dredDuration ?? 100
73
- opusEncoder = try OpusEncoder(
74
- sampleRate: config.sampleRate,
75
- channels: config.channels,
76
- bitrate: config.bitrate,
77
- frameSizeMs: config.frameSize,
78
- dredDurationMs: dredDuration
79
- )
61
+ // Create and start the AudioProcessor (encoding thread)
62
+ let proc = AudioProcessor(config: config)
63
+ proc.setOnAudioChunk { [weak self] frames, timestamp, seq, duration, frameCount in
64
+ self?.onAudioChunk?(frames, timestamp, seq, duration, frameCount)
65
+ }
66
+ proc.setOnStarted { [weak self] timestamp, sampleRate, channels, bitrate, frameSize, preSkip in
67
+ self?.onStarted?(timestamp, sampleRate, channels, bitrate, frameSize, preSkip)
68
+ }
69
+ proc.setOnEnd { [weak self] timestamp, totalDuration, totalPackets in
70
+ self?.onEnd?(timestamp, totalDuration, totalPackets)
71
+ }
72
+
73
+ // Debug file
74
+ var debugURL: URL? = nil
75
+ if config.saveDebugAudio == true {
76
+ let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
77
+ let timestamp = Date().timeIntervalSince1970
78
+ debugURL = documentsPath.appendingPathComponent("debug_pcm_\(timestamp).raw")
79
+ }
80
+
81
+ try proc.start(debugFileURL: debugURL)
82
+ processor = proc
80
83
 
81
84
  // Create and configure AVAudioEngine
82
85
  audioEngine = AVAudioEngine()
@@ -114,13 +117,13 @@ class AudioEngineManager {
114
117
  // Calculate buffer size for one frame
115
118
  let bufferSize: AVAudioFrameCount = 1024
116
119
 
117
- // Install tap on input node with hardware format
120
+ // Install tap real-time thread callback, only does format convert + copy + post
118
121
  inputNode.installTap(
119
122
  onBus: 0,
120
123
  bufferSize: bufferSize,
121
124
  format: hardwareFormat
122
- ) { [weak self] buffer, time in
123
- self?.processBuffer(buffer, time: time)
125
+ ) { [weak self] buffer, _ in
126
+ self?.onTapBuffer(buffer)
124
127
  }
125
128
 
126
129
  // Start audio engine
@@ -128,28 +131,7 @@ class AudioEngineManager {
128
131
 
129
132
  isRecording = true
130
133
 
131
- // Create debug output files if enabled
132
- if config.saveDebugAudio == true {
133
- do {
134
- let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
135
- let timestamp = Date().timeIntervalSince1970
136
-
137
- pcmFileURL = documentsPath.appendingPathComponent("debug_pcm_\(timestamp).raw")
138
-
139
- // Create PCM file
140
- FileManager.default.createFile(atPath: pcmFileURL!.path, contents: nil, attributes: nil)
141
-
142
- // Open file handle for writing
143
- pcmFileHandle = try FileHandle(forWritingTo: pcmFileURL!)
144
-
145
- print("[AudioEngineManager] Debug audio file created:")
146
- print(" PCM: \(pcmFileURL!.path)")
147
- } catch {
148
- print("[AudioEngineManager] Failed to create debug files: \(error)")
149
- }
150
- }
151
-
152
- print("[AudioEngineManager] Started recording: \(hardwareFormat.sampleRate)Hz → \(config.sampleRate)Hz, \(config.channels) ch, DRED: \(dredDuration)ms")
134
+ print("[AudioEngineManager] Started recording: \(hardwareFormat.sampleRate)Hz \(config.sampleRate)Hz, \(config.channels) ch")
153
135
  }
154
136
 
155
137
  func stop() {
@@ -157,26 +139,21 @@ class AudioEngineManager {
157
139
  return
158
140
  }
159
141
 
160
- // Remove tap and stop engine
142
+ isRecording = false
143
+
144
+ // Stop audio capture
161
145
  inputNode?.removeTap(onBus: 0)
162
146
  audioEngine?.stop()
163
147
 
148
+ // Flush and stop the encoding thread (synchronous — drains remaining samples,
149
+ // pads the final partial frame with silence, and emits audioEnd)
150
+ processor?.flushAndStop()
151
+ processor = nil
152
+
164
153
  // Clean up
165
154
  audioEngine = nil
166
155
  inputNode = nil
167
- opusEncoder = nil
168
156
  audioConverter = nil
169
- frameBuffer.removeAll()
170
-
171
- isRecording = false
172
- sequenceNumber = 0
173
-
174
- // Close debug file handle
175
- if let fileHandle = pcmFileHandle {
176
- fileHandle.closeFile()
177
- pcmFileHandle = nil
178
- print("[AudioEngineManager] Closed PCM debug file: \(pcmFileURL?.path ?? "")")
179
- }
180
157
 
181
158
  print("[AudioEngineManager] Stopped recording")
182
159
  }
@@ -193,10 +170,18 @@ class AudioEngineManager {
193
170
 
194
171
  // MARK: - Event Handlers
195
172
 
196
- func setOnAudioChunk(_ callback: @escaping (Data, Double, Int) -> Void) {
173
+ func setOnAudioChunk(_ callback: @escaping ([EncodedFrame], Double, Int, Double, Int) -> Void) {
197
174
  self.onAudioChunk = callback
198
175
  }
199
176
 
177
+ func setOnStarted(_ callback: @escaping (_ timestamp: Double, _ sampleRate: Int, _ channels: Int, _ bitrate: Int, _ frameSize: Double, _ preSkip: Int) -> Void) {
178
+ self.onStarted = callback
179
+ }
180
+
181
+ func setOnEnd(_ callback: @escaping (_ timestamp: Double, _ totalDuration: Double, _ totalPackets: Int) -> Void) {
182
+ self.onEnd = callback
183
+ }
184
+
200
185
  func setOnAmplitude(_ callback: @escaping (Float, Float, Double) -> Void) {
201
186
  self.onAmplitude = callback
202
187
  }
@@ -205,142 +190,126 @@ class AudioEngineManager {
205
190
  self.onError = callback
206
191
  }
207
192
 
208
- // MARK: - Private Methods
193
+ // MARK: - Real-time Audio Thread (tap callback)
194
+ // Only does format conversion + copy + post. No encoding, no locks.
209
195
 
210
- private func processBuffer(_ buffer: AVAudioPCMBuffer, time: AVAudioTime) {
211
- guard !isPaused else {
212
- return
213
- }
214
-
215
- guard let audioConverter = audioConverter else {
216
- return
217
- }
196
+ private func onTapBuffer(_ buffer: AVAudioPCMBuffer) {
197
+ guard !isPaused else { return }
198
+ guard let audioConverter = audioConverter else { return }
218
199
 
219
200
  // Calculate output buffer size based on sample rate conversion
220
- let outputSampleRate = Double(config.sampleRate)
221
- let inputSampleRate = buffer.format.sampleRate
222
- let sampleRateRatio = outputSampleRate / inputSampleRate
201
+ let sampleRateRatio = Double(config.sampleRate) / buffer.format.sampleRate
223
202
  let outputFrameCapacity = AVAudioFrameCount(Double(buffer.frameLength) * sampleRateRatio)
224
203
 
225
- // Create output buffer in desired format (16kHz, Int16)
204
+ // Create output buffer in desired format (e.g. 16kHz, Int16)
226
205
  guard let outputFormat = audioConverter.outputFormat as? AVAudioFormat,
227
206
  let convertedBuffer = AVAudioPCMBuffer(
228
207
  pcmFormat: outputFormat,
229
208
  frameCapacity: outputFrameCapacity
230
- ) else {
231
- return
232
- }
209
+ ) else { return }
233
210
 
234
211
  // Convert audio from hardware format to desired format
235
212
  var error: NSError?
236
- let inputBlock: AVAudioConverterInputBlock = { inNumPackets, outStatus in
213
+ let inputBlock: AVAudioConverterInputBlock = { _, outStatus in
237
214
  outStatus.pointee = .haveData
238
215
  return buffer
239
216
  }
240
217
 
241
218
  let status = audioConverter.convert(to: convertedBuffer, error: &error, withInputFrom: inputBlock)
219
+ if status == .error { return }
242
220
 
243
- if status == .error {
244
- return
245
- }
246
-
247
- // Process the converted Int16 data
248
- guard let channelData = convertedBuffer.int16ChannelData else {
249
- return
250
- }
221
+ guard let channelData = convertedBuffer.int16ChannelData else { return }
251
222
 
252
223
  let frameLength = Int(convertedBuffer.frameLength)
253
224
  let channelDataPointer = channelData[0]
254
225
 
255
- // DEBUG: Log first buffer to verify conversion
256
226
  if !loggedFirstBuffer {
257
227
  print("[AudioEngineManager] First converted buffer: \(frameLength) samples at \(convertedBuffer.format.sampleRate)Hz")
258
228
  loggedFirstBuffer = true
259
229
  }
260
230
 
261
- // Copy PCM data to frame buffer
262
- let frame = Array(UnsafeBufferPointer(start: channelDataPointer, count: frameLength))
263
-
264
- // Save PCM to debug file if enabled
265
- if let fileHandle = pcmFileHandle {
266
- let data = Data(bytes: channelDataPointer, count: frameLength * MemoryLayout<Int16>.size)
267
- fileHandle.write(data)
268
- }
269
-
270
- frameBuffer.append(frame)
271
-
272
- // Calculate how many samples we need for one packet
273
- let samplesPerPacket = Int(Double(config.sampleRate) * config.packetDuration / 1000.0)
274
- let currentSampleCount = frameBuffer.reduce(0) { $0 + $1.count }
275
-
276
- // When we have enough samples for a packet, encode and send
277
- if currentSampleCount >= samplesPerPacket {
278
- encodeAndSendPacket(timestamp: time.sampleTime)
279
- }
231
+ // Copy the PCM and post it to the encoding queue (does not block the tap)
232
+ let samples = Array(UnsafeBufferPointer(start: channelDataPointer, count: frameLength))
233
+ processor?.pushSamples(samples)
280
234
  }
281
235
 
282
- private func encodeAndSendPacket(timestamp: AVAudioFramePosition) {
283
- guard let opusEncoder = opusEncoder else {
284
- return
285
- }
286
-
287
- // Flatten frame buffer into continuous PCM data
288
- let pcmData = frameBuffer.flatMap { $0 }
289
-
290
- // Calculate samples per frame
291
- let samplesPerFrame = Int(Double(config.sampleRate) * config.frameSize / 1000.0)
292
-
293
- // We should only encode when we have at least one frame
294
- guard pcmData.count >= samplesPerFrame else {
295
- return
296
- }
236
+ // MARK: - Audio Session
297
237
 
298
- // Take only ONE frame worth of samples
299
- let frameData = Array(pcmData.prefix(samplesPerFrame))
238
+ private func configureAudioSession() throws {
239
+ let audioSession = AVAudioSession.sharedInstance()
300
240
 
301
- // Encode this single frame to Opus (with DRED padding if enabled)
302
- var encodedPacket: Data?
303
- frameData.withUnsafeBufferPointer { bufferPointer in
304
- guard let baseAddress = bufferPointer.baseAddress else {
305
- return
241
+ let sessionConfig = config.iosAudioSession
242
+ let category = Self.mapCategory(sessionConfig?.category)
243
+ let mode = Self.mapMode(sessionConfig?.mode)
244
+ let options = Self.mapOptions(sessionConfig?.options)
245
+
246
+ // Try the requested config first; if it is an invalid combination, fall back to safe defaults
247
+ do {
248
+ try audioSession.setCategory(category, mode: mode, options: options)
249
+ print("[AudioEngineManager] Audio session configured: category=\(category.rawValue), mode=\(mode.rawValue), options=\(options.rawValue)")
250
+ } catch {
251
+ print("[AudioEngineManager] Custom audio session config failed (\(error.localizedDescription)), falling back to defaults")
252
+ do {
253
+ try audioSession.setCategory(.record, mode: .measurement, options: [])
254
+ print("[AudioEngineManager] Audio session configured with defaults: category=record, mode=measurement, options=[]")
255
+ } catch {
256
+ print("[AudioEngineManager] Fallback audio session config also failed: \(error.localizedDescription), continuing with current session")
306
257
  }
307
-
308
- encodedPacket = opusEncoder.encode(pcm: baseAddress, frameSize: samplesPerFrame)
309
258
  }
310
259
 
311
- guard let opusData = encodedPacket, !opusData.isEmpty else {
312
- print("[AudioEngineManager] Failed to encode Opus packet")
313
- frameBuffer.removeAll()
314
- return
315
- }
316
-
317
- // Calculate timestamp in milliseconds
318
- let timestampMs = Date().timeIntervalSince1970 * 1000
260
+ // setPreferredSampleRate / setPreferredIOBufferDuration are hints, not hard requirements — don't let them crash
261
+ do { try audioSession.setPreferredSampleRate(Double(config.sampleRate)) }
262
+ catch { print("[AudioEngineManager] setPreferredSampleRate failed: \(error.localizedDescription)") }
319
263
 
320
- // Emit audioChunk event with Opus packet (may be larger due to DRED)
321
- onAudioChunk?(opusData, timestampMs, sequenceNumber)
264
+ do { try audioSession.setPreferredIOBufferDuration(config.frameSize / 1000.0) }
265
+ catch { print("[AudioEngineManager] setPreferredIOBufferDuration failed: \(error.localizedDescription)") }
322
266
 
323
- sequenceNumber += 1
267
+ try audioSession.setActive(true)
268
+ }
324
269
 
325
- // Keep any remaining samples for next packet
326
- let remainingSamples = pcmData.count - samplesPerFrame
327
- if remainingSamples > 0 {
328
- frameBuffer = [Array(pcmData[samplesPerFrame...])]
329
- } else {
330
- frameBuffer.removeAll()
270
+ // MARK: - String AVAudioSession Mapping
271
+
272
+ private static func mapCategory(_ value: String?) -> AVAudioSession.Category {
273
+ guard let value = value else { return .record }
274
+ switch value {
275
+ case "record": return .record
276
+ case "playAndRecord": return .playAndRecord
277
+ case "playback": return .playback
278
+ case "ambient": return .ambient
279
+ default:
280
+ print("[AudioEngineManager] Unknown category '\(value)', falling back to .record")
281
+ return .record
331
282
  }
332
283
  }
333
284
 
334
- private func configureAudioSession() throws {
335
- let audioSession = AVAudioSession.sharedInstance()
336
-
337
- try audioSession.setCategory(.record, mode: .measurement, options: [])
338
- try audioSession.setPreferredSampleRate(Double(config.sampleRate))
339
- try audioSession.setPreferredIOBufferDuration(config.frameSize / 1000.0)
340
-
341
- try audioSession.setActive(true)
285
+ private static func mapMode(_ value: String?) -> AVAudioSession.Mode {
286
+ guard let value = value else { return .measurement }
287
+ switch value {
288
+ case "default": return .default
289
+ case "voiceChat": return .voiceChat
290
+ case "measurement": return .measurement
291
+ case "spokenAudio": return .spokenAudio
292
+ default:
293
+ print("[AudioEngineManager] Unknown mode '\(value)', falling back to .measurement")
294
+ return .measurement
295
+ }
296
+ }
342
297
 
343
- print("[AudioEngineManager] Audio session configured")
298
+ private static func mapOptions(_ values: [String]?) -> AVAudioSession.CategoryOptions {
299
+ guard let values = values else { return [] }
300
+ var options: AVAudioSession.CategoryOptions = []
301
+ for value in values {
302
+ switch value {
303
+ case "mixWithOthers": options.insert(.mixWithOthers)
304
+ case "defaultToSpeaker": options.insert(.defaultToSpeaker)
305
+ case "allowBluetooth": options.insert(.allowBluetooth)
306
+ case "allowAirPlay": options.insert(.allowAirPlay)
307
+ case "allowBluetoothA2DP": options.insert(.allowBluetoothA2DP)
308
+ default:
309
+ print("[AudioEngineManager] Unknown option '\(value)', skipping")
310
+ }
311
+ }
312
+ return options
344
313
  }
345
314
 
346
315
  @objc private func handleInterruption(_ notification: Notification) {