@edkimmel/expo-audio-stream 0.4.1 → 0.5.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.
- package/NATIVE_EVENTS.md +37 -0
- package/README.md +13 -0
- package/android/src/main/java/expo/modules/audiostream/ExpoPlayAudioStreamModule.kt +5 -0
- package/android/src/main/java/expo/modules/audiostream/pipeline/AudioPipeline.kt +152 -10
- package/android/src/main/java/expo/modules/audiostream/pipeline/PipelineIntegration.kt +16 -0
- package/build/events.d.ts +4 -0
- package/build/events.d.ts.map +1 -1
- package/build/events.js.map +1 -1
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +8 -3
- package/build/index.js.map +1 -1
- package/build/pipeline/index.d.ts +14 -1
- package/build/pipeline/index.d.ts.map +1 -1
- package/build/pipeline/index.js +15 -0
- package/build/pipeline/index.js.map +1 -1
- package/build/pipeline/types.d.ts +34 -0
- package/build/pipeline/types.d.ts.map +1 -1
- package/build/pipeline/types.js.map +1 -1
- package/build/types.d.ts +8 -0
- package/build/types.d.ts.map +1 -1
- package/build/types.js.map +1 -1
- package/ios/AudioPipeline.swift +67 -2
- package/ios/ExpoPlayAudioStreamModule.swift +40 -0
- package/ios/Microphone.swift +116 -25
- package/ios/PipelineIntegration.swift +11 -1
- package/package.json +1 -2
- package/plugin/build/index.js +5 -0
- package/plugin/src/index.ts +5 -0
- package/src/events.ts +4 -0
- package/src/index.ts +12 -2
- package/src/pipeline/index.ts +17 -0
- package/src/pipeline/types.ts +36 -0
- package/src/types.ts +9 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/pipeline/index.ts"],"names":[],"mappings":"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","sourcesContent":["// ────────────────────────────────────────────────────────────────────────────\n// Native Audio Pipeline — V3 TypeScript Wrapper\n// ────────────────────────────────────────────────────────────────────────────\n//\n// Thin wrapper over the existing ExpoPlayAudioStreamModule (not a new native\n// module). Uses static methods matching the existing codebase pattern.\n//\n// Hot path: pushAudioSync() — synchronous Function call, no Promise overhead.\n// Cold path: pushAudio() — async with error propagation via Promise.\n\nimport type { EventSubscription } from 'expo-modules-core';\nimport ExpoPlayAudioStreamModule from '../ExpoPlayAudioStreamModule';\nimport { subscribeToEvent } from '../events';\n\nimport type {\n ConnectPipelineOptions,\n ConnectPipelineResult,\n PushPipelineAudioOptions,\n InvalidatePipelineTurnOptions,\n PipelineState,\n PipelineEventMap,\n PipelineEventName,\n PipelineTelemetry,\n} from './types';\n\nexport class Pipeline {\n // ════════════════════════════════════════════════════════════════════════\n // Lifecycle\n // ════════════════════════════════════════════════════════════════════════\n\n /**\n * Connect the native audio pipeline.\n *\n * Creates an AudioTrack (buffer size from device HAL), jitter buffer, and\n * MAX_PRIORITY write thread. Config is immutable per session — disconnect\n * and reconnect to change sample rate.\n */\n static async connect(\n options: ConnectPipelineOptions = {}\n ): Promise<ConnectPipelineResult> {\n return await ExpoPlayAudioStreamModule.connectPipeline(options);\n }\n\n /**\n * Disconnect the pipeline. Tears down AudioTrack, write thread, audio\n * focus, volume guard, and zombie detection.\n */\n static async disconnect(): Promise<void> {\n return await ExpoPlayAudioStreamModule.disconnectPipeline();\n }\n\n // ════════════════════════════════════════════════════════════════════════\n // Push audio\n // ════════════════════════════════════════════════════════════════════════\n\n /**\n * Push base64-encoded PCM16 audio into the jitter buffer (async).\n *\n * Use this when you need error propagation via Promise rejection.\n * For the hot path (e.g., inside a WebSocket message handler), prefer\n * [pushAudioSync] which avoids Promise overhead.\n */\n static async pushAudio(options: PushPipelineAudioOptions): Promise<void> {\n return await ExpoPlayAudioStreamModule.pushPipelineAudio(options);\n }\n\n /**\n * Push base64-encoded PCM16 audio synchronously (no Promise overhead).\n *\n * Designed for the hot path — call this from your WebSocket onmessage\n * handler for minimum latency. Returns `true` on success, `false` on\n * failure (errors are also reported via PipelineError events).\n */\n static pushAudioSync(options: PushPipelineAudioOptions): boolean {\n return ExpoPlayAudioStreamModule.pushPipelineAudioSync(options);\n }\n\n // ════════════════════════════════════════════════════════════════════════\n // Turn management\n // ════════════════════════════════════════════════════════════════════════\n\n /**\n * Invalidate the current turn. Resets the jitter buffer so stale audio\n * from the old turn is discarded immediately.\n */\n static async invalidateTurn(\n options: InvalidatePipelineTurnOptions\n ): Promise<void> {\n return await ExpoPlayAudioStreamModule.invalidatePipelineTurn(options);\n }\n\n // ════════════════════════════════════════════════════════════════════════\n // State & Telemetry\n // ════════════════════════════════════════════════════════════════════════\n\n /** Get the current pipeline state synchronously. */\n static getState(): PipelineState {\n return ExpoPlayAudioStreamModule.getPipelineState() as PipelineState;\n }\n\n /** Get a telemetry snapshot (buffer levels, counters, etc.). */\n static getTelemetry(): PipelineTelemetry {\n return ExpoPlayAudioStreamModule.getPipelineTelemetry() as PipelineTelemetry;\n }\n\n // ════════════════════════════════════════════════════════════════════════\n // Event subscriptions\n // ════════════════════════════════════════════════════════════════════════\n\n /**\n * Subscribe to a specific pipeline event with full type safety.\n *\n * @example\n * ```ts\n * const sub = Pipeline.subscribe('PipelineStateChanged', async (e) => {\n * console.log('State:', e.state);\n * });\n * // Later:\n * sub.remove();\n * ```\n */\n static subscribe<K extends PipelineEventName>(\n eventName: K,\n listener: (event: PipelineEventMap[K]) => Promise<void> | void\n ): EventSubscription {\n return subscribeToEvent<PipelineEventMap[K]>(\n eventName,\n async (event) => {\n if (event !== undefined) {\n await listener(event);\n }\n }\n );\n }\n\n /**\n * Convenience: subscribe to both PipelineError and PipelineZombieDetected.\n *\n * Useful for a single error handler that covers fatal and near-fatal\n * conditions. The callback receives a normalized `{ code, message }`.\n */\n static onError(\n listener: (error: { code: string; message: string }) => void\n ): { remove: () => void } {\n const subs: EventSubscription[] = [];\n\n subs.push(\n Pipeline.subscribe('PipelineError', async (e) => {\n listener({ code: e.code, message: e.message });\n })\n );\n\n subs.push(\n Pipeline.subscribe('PipelineZombieDetected', async (e) => {\n listener({\n code: 'ZOMBIE_DETECTED',\n message: `AudioTrack stalled for ${e.stalledMs}ms at head=${e.playbackHead}`,\n });\n })\n );\n\n return {\n remove: () => subs.forEach((s) => s.remove()),\n };\n }\n\n /**\n * Convenience: subscribe to audio focus loss and resumption events.\n *\n * During focus loss the pipeline writes silence instead of real audio.\n * The caller should typically invalidateTurn + re-request audio from the\n * AI backend on focus regain.\n */\n static onAudioFocus(\n listener: (event: { focused: boolean }) => void\n ): { remove: () => void } {\n const subs: EventSubscription[] = [];\n\n subs.push(\n Pipeline.subscribe('PipelineAudioFocusLost', async () => {\n listener({ focused: false });\n })\n );\n\n subs.push(\n Pipeline.subscribe('PipelineAudioFocusResumed', async () => {\n listener({ focused: true });\n })\n );\n\n return {\n remove: () => subs.forEach((s) => s.remove()),\n };\n }\n}\n\n// Re-export all types for consumer convenience\nexport type {\n ConnectPipelineOptions,\n ConnectPipelineResult,\n PushPipelineAudioOptions,\n InvalidatePipelineTurnOptions,\n PipelineState,\n PipelineEventMap,\n PipelineEventName,\n PipelineBufferTelemetry,\n PipelineTelemetry,\n PipelineStateChangedEvent,\n PipelinePlaybackStartedEvent,\n PipelineErrorEvent,\n PipelineZombieDetectedEvent,\n PipelineUnderrunEvent,\n PipelineDrainedEvent,\n PipelineAudioFocusLostEvent,\n PipelineAudioFocusResumedEvent,\n} from './types';\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/pipeline/index.ts"],"names":[],"mappings":"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","sourcesContent":["// ────────────────────────────────────────────────────────────────────────────\n// Native Audio Pipeline — V3 TypeScript Wrapper\n// ────────────────────────────────────────────────────────────────────────────\n//\n// Thin wrapper over the existing ExpoPlayAudioStreamModule (not a new native\n// module). Uses static methods matching the existing codebase pattern.\n//\n// Hot path: pushAudioSync() — synchronous Function call, no Promise overhead.\n// Cold path: pushAudio() — async with error propagation via Promise.\n\nimport type { EventSubscription } from 'expo-modules-core';\nimport ExpoPlayAudioStreamModule from '../ExpoPlayAudioStreamModule';\nimport { subscribeToEvent } from '../events';\n\nimport type {\n ConnectPipelineOptions,\n ConnectPipelineResult,\n PushPipelineAudioOptions,\n InvalidatePipelineTurnOptions,\n PipelineState,\n PipelineEventMap,\n PipelineEventName,\n PipelineTelemetry,\n} from './types';\n\nexport class Pipeline {\n // ════════════════════════════════════════════════════════════════════════\n // Lifecycle\n // ════════════════════════════════════════════════════════════════════════\n\n /**\n * Connect the native audio pipeline.\n *\n * Creates an AudioTrack (buffer size from device HAL), jitter buffer, and\n * MAX_PRIORITY write thread. Config is immutable per session — disconnect\n * and reconnect to change sample rate.\n */\n static async connect(\n options: ConnectPipelineOptions = {}\n ): Promise<ConnectPipelineResult> {\n return await ExpoPlayAudioStreamModule.connectPipeline(options);\n }\n\n /**\n * Disconnect the pipeline. Tears down AudioTrack, write thread, audio\n * focus, volume guard, and zombie detection.\n */\n static async disconnect(): Promise<void> {\n return await ExpoPlayAudioStreamModule.disconnectPipeline();\n }\n\n // ════════════════════════════════════════════════════════════════════════\n // Push audio\n // ════════════════════════════════════════════════════════════════════════\n\n /**\n * Push base64-encoded PCM16 audio into the jitter buffer (async).\n *\n * Use this when you need error propagation via Promise rejection.\n * For the hot path (e.g., inside a WebSocket message handler), prefer\n * [pushAudioSync] which avoids Promise overhead.\n */\n static async pushAudio(options: PushPipelineAudioOptions): Promise<void> {\n return await ExpoPlayAudioStreamModule.pushPipelineAudio(options);\n }\n\n /**\n * Push base64-encoded PCM16 audio synchronously (no Promise overhead).\n *\n * Designed for the hot path — call this from your WebSocket onmessage\n * handler for minimum latency. Returns `true` on success, `false` on\n * failure (errors are also reported via PipelineError events).\n */\n static pushAudioSync(options: PushPipelineAudioOptions): boolean {\n return ExpoPlayAudioStreamModule.pushPipelineAudioSync(options);\n }\n\n // ════════════════════════════════════════════════════════════════════════\n // Turn management\n // ════════════════════════════════════════════════════════════════════════\n\n /**\n * Invalidate the current turn. Resets the jitter buffer so stale audio\n * from the old turn is discarded immediately.\n */\n static async invalidateTurn(\n options: InvalidatePipelineTurnOptions\n ): Promise<void> {\n return await ExpoPlayAudioStreamModule.invalidatePipelineTurn(options);\n }\n\n // ════════════════════════════════════════════════════════════════════════\n // State & Telemetry\n // ════════════════════════════════════════════════════════════════════════\n\n /** Get the current pipeline state synchronously. */\n static getState(): PipelineState {\n return ExpoPlayAudioStreamModule.getPipelineState() as PipelineState;\n }\n\n /** Get a telemetry snapshot (buffer levels, counters, etc.). */\n static getTelemetry(): PipelineTelemetry {\n return ExpoPlayAudioStreamModule.getPipelineTelemetry() as PipelineTelemetry;\n }\n\n /**\n * Query the platform's current output latency — i.e., how long after a\n * sample is written to the native buffer before it actually leaves the\n * speaker.\n *\n * Value can change mid-session, notably on audio route changes such as\n * switching from built-in speaker to Bluetooth (Bluetooth typically adds\n * 100+ ms). **Always query at the moment you care; do not cache.**\n *\n * Returns 0 if the pipeline is not connected or the platform cannot\n * report a value.\n */\n static getOutputLatencyMs(): number {\n return ExpoPlayAudioStreamModule.getPipelineOutputLatencyMs() as number;\n }\n\n // ════════════════════════════════════════════════════════════════════════\n // Event subscriptions\n // ════════════════════════════════════════════════════════════════════════\n\n /**\n * Subscribe to a specific pipeline event with full type safety.\n *\n * @example\n * ```ts\n * const sub = Pipeline.subscribe('PipelineStateChanged', async (e) => {\n * console.log('State:', e.state);\n * });\n * // Later:\n * sub.remove();\n * ```\n */\n static subscribe<K extends PipelineEventName>(\n eventName: K,\n listener: (event: PipelineEventMap[K]) => Promise<void> | void\n ): EventSubscription {\n return subscribeToEvent<PipelineEventMap[K]>(\n eventName,\n async (event) => {\n if (event !== undefined) {\n await listener(event);\n }\n }\n );\n }\n\n /**\n * Convenience: subscribe to both PipelineError and PipelineZombieDetected.\n *\n * Useful for a single error handler that covers fatal and near-fatal\n * conditions. The callback receives a normalized `{ code, message }`.\n */\n static onError(\n listener: (error: { code: string; message: string }) => void\n ): { remove: () => void } {\n const subs: EventSubscription[] = [];\n\n subs.push(\n Pipeline.subscribe('PipelineError', async (e) => {\n listener({ code: e.code, message: e.message });\n })\n );\n\n subs.push(\n Pipeline.subscribe('PipelineZombieDetected', async (e) => {\n listener({\n code: 'ZOMBIE_DETECTED',\n message: `AudioTrack stalled for ${e.stalledMs}ms at head=${e.playbackHead}`,\n });\n })\n );\n\n return {\n remove: () => subs.forEach((s) => s.remove()),\n };\n }\n\n /**\n * Convenience: subscribe to audio focus loss and resumption events.\n *\n * During focus loss the pipeline writes silence instead of real audio.\n * The caller should typically invalidateTurn + re-request audio from the\n * AI backend on focus regain.\n */\n static onAudioFocus(\n listener: (event: { focused: boolean }) => void\n ): { remove: () => void } {\n const subs: EventSubscription[] = [];\n\n subs.push(\n Pipeline.subscribe('PipelineAudioFocusLost', async () => {\n listener({ focused: false });\n })\n );\n\n subs.push(\n Pipeline.subscribe('PipelineAudioFocusResumed', async () => {\n listener({ focused: true });\n })\n );\n\n return {\n remove: () => subs.forEach((s) => s.remove()),\n };\n }\n}\n\n// Re-export all types for consumer convenience\nexport type {\n ConnectPipelineOptions,\n ConnectPipelineResult,\n PushPipelineAudioOptions,\n InvalidatePipelineTurnOptions,\n PipelineState,\n PipelineEventMap,\n PipelineEventName,\n PipelineBufferTelemetry,\n PipelineTelemetry,\n PipelineStateChangedEvent,\n PipelinePlaybackStartedEvent,\n PipelineErrorEvent,\n PipelineZombieDetectedEvent,\n PipelineUnderrunEvent,\n PipelineDrainedEvent,\n PipelinePlaybackStoppedEvent,\n PipelineAudioFocusLostEvent,\n PipelineAudioFocusResumedEvent,\n} from './types';\n"]}
|
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
import { PlaybackMode, FrequencyBandConfig, FrequencyBands } from "../types";
|
|
2
|
+
/**
|
|
3
|
+
* How the pipeline's playback should coexist with other audio on the device.
|
|
4
|
+
*
|
|
5
|
+
* - `'mixWithOthers'` (default): plays alongside other apps without
|
|
6
|
+
* interrupting them. On Android no audio focus is requested. Best for
|
|
7
|
+
* sound effects and short clips.
|
|
8
|
+
* - `'duckOthers'`: requests audio focus with ducking. Other apps lower
|
|
9
|
+
* their volume but keep playing.
|
|
10
|
+
* - `'doNotMix'`: requests exclusive audio focus. Other apps pause.
|
|
11
|
+
*/
|
|
12
|
+
export type PipelineAudioMode = 'mixWithOthers' | 'duckOthers' | 'doNotMix';
|
|
2
13
|
/** Options passed to `connectPipeline()`. */
|
|
3
14
|
export interface ConnectPipelineOptions {
|
|
4
15
|
/** Sample rate in Hz (default 24000). */
|
|
@@ -18,6 +29,15 @@ export interface ConnectPipelineOptions {
|
|
|
18
29
|
frequencyBandIntervalMs?: number;
|
|
19
30
|
/** Optional frequency band crossover configuration. */
|
|
20
31
|
frequencyBandConfig?: FrequencyBandConfig;
|
|
32
|
+
/**
|
|
33
|
+
* How pipeline playback should coexist with other apps' audio.
|
|
34
|
+
* Default is `'mixWithOthers'` (matches expo-audio).
|
|
35
|
+
*
|
|
36
|
+
* Note: this is a **behavior change** vs. prior versions of this library,
|
|
37
|
+
* which effectively used `'doNotMix'`. Pass `'doNotMix'` explicitly to
|
|
38
|
+
* preserve that old behavior.
|
|
39
|
+
*/
|
|
40
|
+
audioMode?: PipelineAudioMode;
|
|
21
41
|
}
|
|
22
42
|
/** Result returned from a successful `connectPipeline()` call. */
|
|
23
43
|
export interface ConnectPipelineResult {
|
|
@@ -83,6 +103,19 @@ export interface PipelineUnderrunEvent {
|
|
|
83
103
|
export interface PipelineDrainedEvent {
|
|
84
104
|
turnId: string;
|
|
85
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* Payload for `PipelinePlaybackStopped`.
|
|
108
|
+
*
|
|
109
|
+
* Fired when the last sample physically leaves the speaker, approximately
|
|
110
|
+
* `outputLatencyMs` after `PipelineDrained` for the same turn. Pairs with
|
|
111
|
+
* `PipelinePlaybackStarted` (start-of-emission ↔ end-of-emission).
|
|
112
|
+
*
|
|
113
|
+
* Note: this is a physical-world milestone, distinct from `state: 'idle'`
|
|
114
|
+
* (the pipeline-state-machine value reported via `PipelineStateChanged`).
|
|
115
|
+
*/
|
|
116
|
+
export interface PipelinePlaybackStoppedEvent {
|
|
117
|
+
turnId: string;
|
|
118
|
+
}
|
|
86
119
|
/** Payload for `PipelineAudioFocusLost` (empty — presence is the signal). */
|
|
87
120
|
export type PipelineAudioFocusLostEvent = Record<string, never>;
|
|
88
121
|
/** Payload for `PipelineAudioFocusResumed` (empty — presence is the signal). */
|
|
@@ -101,6 +134,7 @@ export interface PipelineEventMap {
|
|
|
101
134
|
PipelineZombieDetected: PipelineZombieDetectedEvent;
|
|
102
135
|
PipelineUnderrun: PipelineUnderrunEvent;
|
|
103
136
|
PipelineDrained: PipelineDrainedEvent;
|
|
137
|
+
PipelinePlaybackStopped: PipelinePlaybackStoppedEvent;
|
|
104
138
|
PipelineAudioFocusLost: PipelineAudioFocusLostEvent;
|
|
105
139
|
PipelineAudioFocusResumed: PipelineAudioFocusResumedEvent;
|
|
106
140
|
PipelineFrequencyBands: PipelineFrequencyBandsEvent;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/pipeline/types.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,YAAY,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAI7E,6CAA6C;AAC7C,MAAM,WAAW,sBAAsB;IACrC,yCAAyC;IACzC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,6DAA6D;IAC7D,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;OAEG;IACH,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,sEAAsE;IACtE,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,uDAAuD;IACvD,mBAAmB,CAAC,EAAE,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/pipeline/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/pipeline/types.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,8CAA8C;AAC9C,+EAA+E","sourcesContent":["// ────────────────────────────────────────────────────────────────────────────\n// Native Audio Pipeline — V3 TypeScript Types\n// ────────────────────────────────────────────────────────────────────────────\n\nimport { PlaybackMode, FrequencyBandConfig, FrequencyBands } from \"../types\";\n\n// ── Connect ─────────────────────────────────────────────────────────────────\n\n/** Options passed to `connectPipeline()`. */\nexport interface ConnectPipelineOptions {\n /** Sample rate in Hz (default 24000). */\n sampleRate?: number;\n /** Number of channels — 1 = mono, 2 = stereo (default 1). */\n channelCount?: number;\n /**\n * How many ms of audio to accumulate in the jitter buffer before the\n * priming gate opens and audio begins playing (default 80).\n */\n targetBufferMs?: number;\n /**\n * Playback mode hint for native optimizations. Affects thread priority and\n */\n playbackMode?: PlaybackMode;\n /** Interval in ms for PipelineFrequencyBands events (default 100). */\n frequencyBandIntervalMs?: number;\n /** Optional frequency band crossover configuration. */\n frequencyBandConfig?: FrequencyBandConfig;\n}\n\n/** Result returned from a successful `connectPipeline()` call. */\nexport interface ConnectPipelineResult {\n sampleRate: number;\n channelCount: number;\n targetBufferMs: number;\n /**\n * Frame size in samples derived from the device HAL's\n * `AudioTrack.getMinBufferSize()`. Useful for understanding the write\n * granularity on the native side.\n */\n frameSizeSamples: number;\n}\n\n// ── Push Audio ──────────────────────────────────────────────────────────────\n\n/** Options passed to `pushPipelineAudio()` / `pushPipelineAudioSync()`. */\nexport interface PushPipelineAudioOptions {\n /** Base64-encoded PCM 16-bit signed LE audio data. */\n audio: string;\n /** Conversation turn identifier. */\n turnId: string;\n /** True if this is the first chunk of a new turn (resets jitter buffer). */\n isFirstChunk?: boolean;\n /** True if this is the final chunk of the current turn (marks end-of-stream). */\n isLastChunk?: boolean;\n}\n\n// ── Invalidate Turn ─────────────────────────────────────────────────────────\n\n/** Options passed to `invalidatePipelineTurn()`. */\nexport interface InvalidatePipelineTurnOptions {\n /** The new turn identifier — stale audio for the old turn is discarded. */\n turnId: string;\n}\n\n// ── State ───────────────────────────────────────────────────────────────────\n\n/**\n * Pipeline states reported via `PipelineStateChanged` events.\n *\n * - `idle` — connected but no audio flowing\n * - `connecting` — AudioTrack being created, focus being requested\n * - `streaming` — actively receiving and playing audio\n * - `draining` — end-of-stream marked, playing remaining buffer\n * - `error` — unrecoverable error (zombie, write failure, etc.)\n */\nexport type PipelineState =\n | 'idle'\n | 'connecting'\n | 'streaming'\n | 'draining'\n | 'error';\n\n// ── Events ──────────────────────────────────────────────────────────────────\n\n/** Payload for `PipelineStateChanged`. */\nexport interface PipelineStateChangedEvent {\n state: PipelineState;\n}\n\n/** Payload for `PipelinePlaybackStarted`. */\nexport interface PipelinePlaybackStartedEvent {\n turnId: string;\n}\n\n/** Payload for `PipelineError`. */\nexport interface PipelineErrorEvent {\n code: string;\n message: string;\n}\n\n/** Payload for `PipelineZombieDetected`. */\nexport interface PipelineZombieDetectedEvent {\n playbackHead: number;\n stalledMs: number;\n}\n\n/** Payload for `PipelineUnderrun`. */\nexport interface PipelineUnderrunEvent {\n count: number;\n}\n\n/** Payload for `PipelineDrained`. */\nexport interface PipelineDrainedEvent {\n turnId: string;\n}\n\n/** Payload for `PipelineAudioFocusLost` (empty — presence is the signal). */\nexport type PipelineAudioFocusLostEvent = Record<string, never>;\n\n/** Payload for `PipelineAudioFocusResumed` (empty — presence is the signal). */\nexport type PipelineAudioFocusResumedEvent = Record<string, never>;\n\n/** Payload for `PipelineFrequencyBands`. */\nexport interface PipelineFrequencyBandsEvent extends FrequencyBands {}\n\n/**\n * Map of all pipeline event names to their payload types.\n * Used with `Pipeline.subscribe<K>()` for type-safe event subscriptions.\n */\nexport interface PipelineEventMap {\n PipelineStateChanged: PipelineStateChangedEvent;\n PipelinePlaybackStarted: PipelinePlaybackStartedEvent;\n PipelineError: PipelineErrorEvent;\n PipelineZombieDetected: PipelineZombieDetectedEvent;\n PipelineUnderrun: PipelineUnderrunEvent;\n PipelineDrained: PipelineDrainedEvent;\n PipelineAudioFocusLost: PipelineAudioFocusLostEvent;\n PipelineAudioFocusResumed: PipelineAudioFocusResumedEvent;\n PipelineFrequencyBands: PipelineFrequencyBandsEvent;\n}\n\n/** Union of all pipeline event name strings. */\nexport type PipelineEventName = keyof PipelineEventMap;\n\n// ── Telemetry ───────────────────────────────────────────────────────────────\n\n/** Jitter buffer telemetry counters. */\nexport interface PipelineBufferTelemetry {\n /** Current buffer level in milliseconds. */\n bufferMs: number;\n /** Current buffer level in samples. */\n bufferSamples: number;\n /** Whether the priming gate has opened. */\n primed: boolean;\n /** Total samples written by the producer since last reset. */\n totalWritten: number;\n /** Total samples read by the consumer since last reset. */\n totalRead: number;\n /** Number of underrun events. */\n underrunCount: number;\n /** Peak buffer level in samples. */\n peakLevel: number;\n}\n\n/** Full pipeline telemetry snapshot. */\nexport interface PipelineTelemetry extends PipelineBufferTelemetry {\n /** Current pipeline state. */\n state: PipelineState;\n /** Total pushAudio/pushAudioSync calls since connect. */\n totalPushCalls: number;\n /** Total bytes pushed since connect. */\n totalPushBytes: number;\n /** Total write-loop iterations since connect. */\n totalWriteLoops: number;\n /** Current turn identifier. */\n turnId: string;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/pipeline/types.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,8CAA8C;AAC9C,+EAA+E","sourcesContent":["// ────────────────────────────────────────────────────────────────────────────\n// Native Audio Pipeline — V3 TypeScript Types\n// ────────────────────────────────────────────────────────────────────────────\n\nimport { PlaybackMode, FrequencyBandConfig, FrequencyBands } from \"../types\";\n\n// ── Connect ─────────────────────────────────────────────────────────────────\n\n/**\n * How the pipeline's playback should coexist with other audio on the device.\n *\n * - `'mixWithOthers'` (default): plays alongside other apps without\n * interrupting them. On Android no audio focus is requested. Best for\n * sound effects and short clips.\n * - `'duckOthers'`: requests audio focus with ducking. Other apps lower\n * their volume but keep playing.\n * - `'doNotMix'`: requests exclusive audio focus. Other apps pause.\n */\nexport type PipelineAudioMode = 'mixWithOthers' | 'duckOthers' | 'doNotMix';\n\n/** Options passed to `connectPipeline()`. */\nexport interface ConnectPipelineOptions {\n /** Sample rate in Hz (default 24000). */\n sampleRate?: number;\n /** Number of channels — 1 = mono, 2 = stereo (default 1). */\n channelCount?: number;\n /**\n * How many ms of audio to accumulate in the jitter buffer before the\n * priming gate opens and audio begins playing (default 80).\n */\n targetBufferMs?: number;\n /**\n * Playback mode hint for native optimizations. Affects thread priority and\n */\n playbackMode?: PlaybackMode;\n /** Interval in ms for PipelineFrequencyBands events (default 100). */\n frequencyBandIntervalMs?: number;\n /** Optional frequency band crossover configuration. */\n frequencyBandConfig?: FrequencyBandConfig;\n /**\n * How pipeline playback should coexist with other apps' audio.\n * Default is `'mixWithOthers'` (matches expo-audio).\n *\n * Note: this is a **behavior change** vs. prior versions of this library,\n * which effectively used `'doNotMix'`. Pass `'doNotMix'` explicitly to\n * preserve that old behavior.\n */\n audioMode?: PipelineAudioMode;\n}\n\n/** Result returned from a successful `connectPipeline()` call. */\nexport interface ConnectPipelineResult {\n sampleRate: number;\n channelCount: number;\n targetBufferMs: number;\n /**\n * Frame size in samples derived from the device HAL's\n * `AudioTrack.getMinBufferSize()`. Useful for understanding the write\n * granularity on the native side.\n */\n frameSizeSamples: number;\n}\n\n// ── Push Audio ──────────────────────────────────────────────────────────────\n\n/** Options passed to `pushPipelineAudio()` / `pushPipelineAudioSync()`. */\nexport interface PushPipelineAudioOptions {\n /** Base64-encoded PCM 16-bit signed LE audio data. */\n audio: string;\n /** Conversation turn identifier. */\n turnId: string;\n /** True if this is the first chunk of a new turn (resets jitter buffer). */\n isFirstChunk?: boolean;\n /** True if this is the final chunk of the current turn (marks end-of-stream). */\n isLastChunk?: boolean;\n}\n\n// ── Invalidate Turn ─────────────────────────────────────────────────────────\n\n/** Options passed to `invalidatePipelineTurn()`. */\nexport interface InvalidatePipelineTurnOptions {\n /** The new turn identifier — stale audio for the old turn is discarded. */\n turnId: string;\n}\n\n// ── State ───────────────────────────────────────────────────────────────────\n\n/**\n * Pipeline states reported via `PipelineStateChanged` events.\n *\n * - `idle` — connected but no audio flowing\n * - `connecting` — AudioTrack being created, focus being requested\n * - `streaming` — actively receiving and playing audio\n * - `draining` — end-of-stream marked, playing remaining buffer\n * - `error` — unrecoverable error (zombie, write failure, etc.)\n */\nexport type PipelineState =\n | 'idle'\n | 'connecting'\n | 'streaming'\n | 'draining'\n | 'error';\n\n// ── Events ──────────────────────────────────────────────────────────────────\n\n/** Payload for `PipelineStateChanged`. */\nexport interface PipelineStateChangedEvent {\n state: PipelineState;\n}\n\n/** Payload for `PipelinePlaybackStarted`. */\nexport interface PipelinePlaybackStartedEvent {\n turnId: string;\n}\n\n/** Payload for `PipelineError`. */\nexport interface PipelineErrorEvent {\n code: string;\n message: string;\n}\n\n/** Payload for `PipelineZombieDetected`. */\nexport interface PipelineZombieDetectedEvent {\n playbackHead: number;\n stalledMs: number;\n}\n\n/** Payload for `PipelineUnderrun`. */\nexport interface PipelineUnderrunEvent {\n count: number;\n}\n\n/** Payload for `PipelineDrained`. */\nexport interface PipelineDrainedEvent {\n turnId: string;\n}\n\n/**\n * Payload for `PipelinePlaybackStopped`.\n *\n * Fired when the last sample physically leaves the speaker, approximately\n * `outputLatencyMs` after `PipelineDrained` for the same turn. Pairs with\n * `PipelinePlaybackStarted` (start-of-emission ↔ end-of-emission).\n *\n * Note: this is a physical-world milestone, distinct from `state: 'idle'`\n * (the pipeline-state-machine value reported via `PipelineStateChanged`).\n */\nexport interface PipelinePlaybackStoppedEvent {\n turnId: string;\n}\n\n/** Payload for `PipelineAudioFocusLost` (empty — presence is the signal). */\nexport type PipelineAudioFocusLostEvent = Record<string, never>;\n\n/** Payload for `PipelineAudioFocusResumed` (empty — presence is the signal). */\nexport type PipelineAudioFocusResumedEvent = Record<string, never>;\n\n/** Payload for `PipelineFrequencyBands`. */\nexport interface PipelineFrequencyBandsEvent extends FrequencyBands {}\n\n/**\n * Map of all pipeline event names to their payload types.\n * Used with `Pipeline.subscribe<K>()` for type-safe event subscriptions.\n */\nexport interface PipelineEventMap {\n PipelineStateChanged: PipelineStateChangedEvent;\n PipelinePlaybackStarted: PipelinePlaybackStartedEvent;\n PipelineError: PipelineErrorEvent;\n PipelineZombieDetected: PipelineZombieDetectedEvent;\n PipelineUnderrun: PipelineUnderrunEvent;\n PipelineDrained: PipelineDrainedEvent;\n PipelinePlaybackStopped: PipelinePlaybackStoppedEvent;\n PipelineAudioFocusLost: PipelineAudioFocusLostEvent;\n PipelineAudioFocusResumed: PipelineAudioFocusResumedEvent;\n PipelineFrequencyBands: PipelineFrequencyBandsEvent;\n}\n\n/** Union of all pipeline event name strings. */\nexport type PipelineEventName = keyof PipelineEventMap;\n\n// ── Telemetry ───────────────────────────────────────────────────────────────\n\n/** Jitter buffer telemetry counters. */\nexport interface PipelineBufferTelemetry {\n /** Current buffer level in milliseconds. */\n bufferMs: number;\n /** Current buffer level in samples. */\n bufferSamples: number;\n /** Whether the priming gate has opened. */\n primed: boolean;\n /** Total samples written by the producer since last reset. */\n totalWritten: number;\n /** Total samples read by the consumer since last reset. */\n totalRead: number;\n /** Number of underrun events. */\n underrunCount: number;\n /** Peak buffer level in samples. */\n peakLevel: number;\n}\n\n/** Full pipeline telemetry snapshot. */\nexport interface PipelineTelemetry extends PipelineBufferTelemetry {\n /** Current pipeline state. */\n state: PipelineState;\n /** Total pushAudio/pushAudioSync calls since connect. */\n totalPushCalls: number;\n /** Total bytes pushed since connect. */\n totalPushBytes: number;\n /** Total write-loop iterations since connect. */\n totalWriteLoops: number;\n /** Current turn identifier. */\n turnId: string;\n}\n"]}
|
package/build/types.d.ts
CHANGED
|
@@ -103,9 +103,17 @@ export interface RecordingConfig {
|
|
|
103
103
|
enableProcessing?: boolean;
|
|
104
104
|
pointsPerSecond?: number;
|
|
105
105
|
onAudioStream?: (event: AudioDataEvent) => Promise<void>;
|
|
106
|
+
/** Fired when the native layer reports a mid-recording error (e.g. system
|
|
107
|
+
* interruption like Siri or a phone call). The consumer should treat the
|
|
108
|
+
* recording session as terminated and clean up. */
|
|
109
|
+
onError?: (event: MicrophoneErrorEvent) => void;
|
|
106
110
|
/** Optional frequency band crossover configuration. */
|
|
107
111
|
frequencyBandConfig?: FrequencyBandConfig;
|
|
108
112
|
}
|
|
113
|
+
export interface MicrophoneErrorEvent {
|
|
114
|
+
code: string;
|
|
115
|
+
message: string;
|
|
116
|
+
}
|
|
109
117
|
export interface Chunk {
|
|
110
118
|
text: string;
|
|
111
119
|
timestamp: [number, number | null];
|
package/build/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/build/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAOA,MAAM,CAAC,MAAM,aAAa,GAAG;IAC3B,OAAO,EAAE,SAAS;IAClB,gBAAgB,EAAE,iBAAiB;IACnC,YAAY,EAAE,cAAc;CACpB,CAAC;AAgCX,MAAM,CAAC,MAAM,aAAa,GAAG;IAC3B,SAAS,EAAE,WAAW;IACtB,SAAS,EAAE,WAAW;CACd,CAAC","sourcesContent":["export type RecordingEncodingType =\n | 'pcm_32bit'\n | 'pcm_16bit'\n | 'pcm_8bit';\nexport type SampleRate = 16000 | 24000 | 44100 | 48000;\nexport type BitDepth = 8 | 16 | 32;\n\nexport const PlaybackModes = {\n REGULAR: 'regular',\n VOICE_PROCESSING: 'voiceProcessing',\n CONVERSATION: 'conversation',\n} as const;\n/**\n * Defines different playback modes for audio processing\n */\nexport type PlaybackMode =\n (typeof PlaybackModes)[keyof typeof PlaybackModes];\n\n/**\n * Configuration for buffered audio streaming\n */\nexport interface BufferedStreamConfig {\n /**\n * Turn ID for queue management\n */\n turnId: string;\n\n /**\n * Audio encoding format\n */\n encoding?: Encoding;\n\n /**\n * Buffer configuration options\n */\n bufferConfig?: Partial<IAudioBufferConfig>;\n\n /**\n * Callback for buffer health updates\n */\n onBufferHealth?: (metrics: IBufferHealthMetrics) => void;\n}\n\nexport const EncodingTypes = {\n PCM_F32LE: 'pcm_f32le',\n PCM_S16LE: 'pcm_s16le',\n} as const;\n\n/**\n * Defines different encoding formats for audio data\n */\nexport type Encoding =\n (typeof EncodingTypes)[keyof typeof EncodingTypes];\n\n/** RMS energy per frequency band, range [0, 1]. */\nexport interface FrequencyBands {\n low: number;\n mid: number;\n high: number;\n}\n\n/** Crossover frequency configuration for band analysis. */\nexport interface FrequencyBandConfig {\n /** Low/mid crossover in Hz (default 300). */\n lowCrossoverHz?: number;\n /** Mid/high crossover in Hz (default 2000). */\n highCrossoverHz?: number;\n}\n\n/**\n * Smart buffering mode options\n */\nexport type SmartBufferMode =\n | 'conservative'\n | 'balanced'\n | 'aggressive'\n | 'adaptive';\n\n/**\n * Network condition indicators for smart buffering\n */\nexport interface NetworkConditions {\n latency?: number; // Round-trip time in ms\n jitter?: number; // Network jitter in ms\n packetLoss?: number; // Packet loss percentage (0-100)\n bandwidth?: number; // Available bandwidth estimate\n}\n\n/**\n * Smart buffering configuration\n */\nexport interface SmartBufferConfig {\n mode: SmartBufferMode;\n networkConditions?: NetworkConditions;\n adaptiveThresholds?: {\n highLatencyMs?: number; // Threshold to enable aggressive buffering\n highJitterMs?: number; // Threshold to increase buffer size\n packetLossPercent?: number; // Threshold to enable buffering\n };\n}\n\nexport interface StartRecordingResult {\n fileUri: string;\n mimeType: string;\n channels?: number;\n bitDepth?: BitDepth;\n sampleRate?: SampleRate;\n}\n\nexport interface AudioDataEvent {\n data: string | Float32Array;\n data16kHz?: string | Float32Array;\n position: number;\n fileUri: string;\n eventDataSize: number;\n totalSize: number;\n soundLevel?: number;\n /** Frequency band RMS energy, present when recording is active. */\n frequencyBands?: FrequencyBands;\n}\n\nexport interface RecordingConfig {\n sampleRate?: SampleRate; // Sample rate for recording\n channels?: 1 | 2; // 1 or 2 (MONO or STEREO)\n encoding?: RecordingEncodingType; // Encoding type for the recording\n interval?: number; // Interval in milliseconds at which to emit recording data\n\n // Optional parameters for audio processing\n enableProcessing?: boolean; // Boolean to enable/disable audio processing (default is false)\n pointsPerSecond?: number; // Number of data points to extract per second of audio (default is 1000)\n onAudioStream?: (event: AudioDataEvent) => Promise<void>; // Callback function to handle audio stream\n /** Optional frequency band crossover configuration. */\n frequencyBandConfig?: FrequencyBandConfig;\n}\n\nexport interface Chunk {\n text: string;\n timestamp: [number, number | null];\n}\n\nexport interface TranscriberData {\n id: string;\n isBusy: boolean;\n text: string;\n startTime: number;\n endTime: number;\n chunks: Chunk[];\n}\n\nexport interface AudioRecording {\n fileUri: string;\n filename: string;\n durationMs: number;\n size: number;\n channels: number;\n bitDepth: BitDepth;\n sampleRate: SampleRate;\n mimeType: string;\n transcripts?: TranscriberData[];\n wavPCMData?: Float32Array; // Full PCM data for the recording in WAV format (only on web, for native use the fileUri)\n}\n\n// Audio Jitter Buffer Types\n\n/**\n * Configuration for audio buffer management\n */\nexport interface IAudioBufferConfig {\n targetBufferMs: number; // Target buffer size in milliseconds\n minBufferMs: number; // Minimum buffer size before underrun handling\n maxBufferMs: number; // Maximum buffer size before overrun handling\n frameIntervalMs: number; // Expected frame interval in milliseconds\n}\n\n/**\n * Audio payload for playback containing base64 encoded audio data\n */\nexport interface IAudioPlayPayload {\n audioData: string; // Base64 encoded PCM audio data\n isFirst?: boolean; // True if this is the first chunk in a stream\n isFinal?: boolean; // True if this is the final chunk in a stream\n}\n\n/**\n * Processed audio frame with metadata\n */\nexport interface IAudioFrame {\n sequenceNumber: number; // Sequential frame number\n data: IAudioPlayPayload; // Original audio payload\n duration: number; // Estimated frame duration in milliseconds\n timestamp: number; // Frame timestamp when processed\n}\n\n/**\n * Buffer health states for quality monitoring\n */\nexport type BufferHealthState =\n | 'idle'\n | 'healthy'\n | 'degraded'\n | 'critical';\n\n/**\n * Comprehensive buffer health and quality metrics\n */\nexport interface IBufferHealthMetrics {\n currentBufferMs: number; // Current buffer level in milliseconds\n targetBufferMs: number; // Target buffer level in milliseconds\n underrunCount: number; // Total number of buffer underruns\n overrunCount: number; // Total number of buffer overruns\n averageJitter: number; // Average network jitter in milliseconds\n bufferHealthState: BufferHealthState; // Current buffer health assessment\n adaptiveAdjustmentsCount: number; // Number of adaptive adjustments made\n}\n\n/**\n * Interface for audio buffer management\n */\nexport interface IAudioBufferManager {\n enqueueFrames(audioData: IAudioPlayPayload): void;\n startPlayback(): void;\n stopPlayback(): void;\n isPlaying(): boolean;\n getHealthMetrics(): IBufferHealthMetrics;\n updateConfig(config: Partial<IAudioBufferConfig>): void;\n applyAdaptiveAdjustments(): void;\n destroy(): void;\n getCurrentBufferMs(): number;\n}\n\n/**\n * Interface for frame processing\n */\nexport interface IFrameProcessor {\n parseChunk(payload: IAudioPlayPayload): IAudioFrame[];\n reset(): void;\n}\n\n/**\n * Interface for quality monitoring\n */\nexport interface IQualityMonitor {\n recordFrameArrival(timestamp: number): void;\n recordUnderrun(): void;\n recordOverrun(): void;\n updateBufferLevel(bufferMs: number): void;\n getMetrics(): IBufferHealthMetrics;\n getBufferHealthState(\n isPlaying: boolean,\n currentLatencyMs: number\n ): BufferHealthState;\n getRecommendedAdjustment(): number;\n reset(): void;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAOA,MAAM,CAAC,MAAM,aAAa,GAAG;IAC3B,OAAO,EAAE,SAAS;IAClB,gBAAgB,EAAE,iBAAiB;IACnC,YAAY,EAAE,cAAc;CACpB,CAAC;AAgCX,MAAM,CAAC,MAAM,aAAa,GAAG;IAC3B,SAAS,EAAE,WAAW;IACtB,SAAS,EAAE,WAAW;CACd,CAAC","sourcesContent":["export type RecordingEncodingType =\n | 'pcm_32bit'\n | 'pcm_16bit'\n | 'pcm_8bit';\nexport type SampleRate = 16000 | 24000 | 44100 | 48000;\nexport type BitDepth = 8 | 16 | 32;\n\nexport const PlaybackModes = {\n REGULAR: 'regular',\n VOICE_PROCESSING: 'voiceProcessing',\n CONVERSATION: 'conversation',\n} as const;\n/**\n * Defines different playback modes for audio processing\n */\nexport type PlaybackMode =\n (typeof PlaybackModes)[keyof typeof PlaybackModes];\n\n/**\n * Configuration for buffered audio streaming\n */\nexport interface BufferedStreamConfig {\n /**\n * Turn ID for queue management\n */\n turnId: string;\n\n /**\n * Audio encoding format\n */\n encoding?: Encoding;\n\n /**\n * Buffer configuration options\n */\n bufferConfig?: Partial<IAudioBufferConfig>;\n\n /**\n * Callback for buffer health updates\n */\n onBufferHealth?: (metrics: IBufferHealthMetrics) => void;\n}\n\nexport const EncodingTypes = {\n PCM_F32LE: 'pcm_f32le',\n PCM_S16LE: 'pcm_s16le',\n} as const;\n\n/**\n * Defines different encoding formats for audio data\n */\nexport type Encoding =\n (typeof EncodingTypes)[keyof typeof EncodingTypes];\n\n/** RMS energy per frequency band, range [0, 1]. */\nexport interface FrequencyBands {\n low: number;\n mid: number;\n high: number;\n}\n\n/** Crossover frequency configuration for band analysis. */\nexport interface FrequencyBandConfig {\n /** Low/mid crossover in Hz (default 300). */\n lowCrossoverHz?: number;\n /** Mid/high crossover in Hz (default 2000). */\n highCrossoverHz?: number;\n}\n\n/**\n * Smart buffering mode options\n */\nexport type SmartBufferMode =\n | 'conservative'\n | 'balanced'\n | 'aggressive'\n | 'adaptive';\n\n/**\n * Network condition indicators for smart buffering\n */\nexport interface NetworkConditions {\n latency?: number; // Round-trip time in ms\n jitter?: number; // Network jitter in ms\n packetLoss?: number; // Packet loss percentage (0-100)\n bandwidth?: number; // Available bandwidth estimate\n}\n\n/**\n * Smart buffering configuration\n */\nexport interface SmartBufferConfig {\n mode: SmartBufferMode;\n networkConditions?: NetworkConditions;\n adaptiveThresholds?: {\n highLatencyMs?: number; // Threshold to enable aggressive buffering\n highJitterMs?: number; // Threshold to increase buffer size\n packetLossPercent?: number; // Threshold to enable buffering\n };\n}\n\nexport interface StartRecordingResult {\n fileUri: string;\n mimeType: string;\n channels?: number;\n bitDepth?: BitDepth;\n sampleRate?: SampleRate;\n}\n\nexport interface AudioDataEvent {\n data: string | Float32Array;\n data16kHz?: string | Float32Array;\n position: number;\n fileUri: string;\n eventDataSize: number;\n totalSize: number;\n soundLevel?: number;\n /** Frequency band RMS energy, present when recording is active. */\n frequencyBands?: FrequencyBands;\n}\n\nexport interface RecordingConfig {\n sampleRate?: SampleRate; // Sample rate for recording\n channels?: 1 | 2; // 1 or 2 (MONO or STEREO)\n encoding?: RecordingEncodingType; // Encoding type for the recording\n interval?: number; // Interval in milliseconds at which to emit recording data\n\n // Optional parameters for audio processing\n enableProcessing?: boolean; // Boolean to enable/disable audio processing (default is false)\n pointsPerSecond?: number; // Number of data points to extract per second of audio (default is 1000)\n onAudioStream?: (event: AudioDataEvent) => Promise<void>; // Callback function to handle audio stream\n /** Fired when the native layer reports a mid-recording error (e.g. system\n * interruption like Siri or a phone call). The consumer should treat the\n * recording session as terminated and clean up. */\n onError?: (event: MicrophoneErrorEvent) => void;\n /** Optional frequency band crossover configuration. */\n frequencyBandConfig?: FrequencyBandConfig;\n}\n\nexport interface MicrophoneErrorEvent {\n code: string;\n message: string;\n}\n\nexport interface Chunk {\n text: string;\n timestamp: [number, number | null];\n}\n\nexport interface TranscriberData {\n id: string;\n isBusy: boolean;\n text: string;\n startTime: number;\n endTime: number;\n chunks: Chunk[];\n}\n\nexport interface AudioRecording {\n fileUri: string;\n filename: string;\n durationMs: number;\n size: number;\n channels: number;\n bitDepth: BitDepth;\n sampleRate: SampleRate;\n mimeType: string;\n transcripts?: TranscriberData[];\n wavPCMData?: Float32Array; // Full PCM data for the recording in WAV format (only on web, for native use the fileUri)\n}\n\n// Audio Jitter Buffer Types\n\n/**\n * Configuration for audio buffer management\n */\nexport interface IAudioBufferConfig {\n targetBufferMs: number; // Target buffer size in milliseconds\n minBufferMs: number; // Minimum buffer size before underrun handling\n maxBufferMs: number; // Maximum buffer size before overrun handling\n frameIntervalMs: number; // Expected frame interval in milliseconds\n}\n\n/**\n * Audio payload for playback containing base64 encoded audio data\n */\nexport interface IAudioPlayPayload {\n audioData: string; // Base64 encoded PCM audio data\n isFirst?: boolean; // True if this is the first chunk in a stream\n isFinal?: boolean; // True if this is the final chunk in a stream\n}\n\n/**\n * Processed audio frame with metadata\n */\nexport interface IAudioFrame {\n sequenceNumber: number; // Sequential frame number\n data: IAudioPlayPayload; // Original audio payload\n duration: number; // Estimated frame duration in milliseconds\n timestamp: number; // Frame timestamp when processed\n}\n\n/**\n * Buffer health states for quality monitoring\n */\nexport type BufferHealthState =\n | 'idle'\n | 'healthy'\n | 'degraded'\n | 'critical';\n\n/**\n * Comprehensive buffer health and quality metrics\n */\nexport interface IBufferHealthMetrics {\n currentBufferMs: number; // Current buffer level in milliseconds\n targetBufferMs: number; // Target buffer level in milliseconds\n underrunCount: number; // Total number of buffer underruns\n overrunCount: number; // Total number of buffer overruns\n averageJitter: number; // Average network jitter in milliseconds\n bufferHealthState: BufferHealthState; // Current buffer health assessment\n adaptiveAdjustmentsCount: number; // Number of adaptive adjustments made\n}\n\n/**\n * Interface for audio buffer management\n */\nexport interface IAudioBufferManager {\n enqueueFrames(audioData: IAudioPlayPayload): void;\n startPlayback(): void;\n stopPlayback(): void;\n isPlaying(): boolean;\n getHealthMetrics(): IBufferHealthMetrics;\n updateConfig(config: Partial<IAudioBufferConfig>): void;\n applyAdaptiveAdjustments(): void;\n destroy(): void;\n getCurrentBufferMs(): number;\n}\n\n/**\n * Interface for frame processing\n */\nexport interface IFrameProcessor {\n parseChunk(payload: IAudioPlayPayload): IAudioFrame[];\n reset(): void;\n}\n\n/**\n * Interface for quality monitoring\n */\nexport interface IQualityMonitor {\n recordFrameArrival(timestamp: number): void;\n recordUnderrun(): void;\n recordOverrun(): void;\n updateBufferLevel(bufferMs: number): void;\n getMetrics(): IBufferHealthMetrics;\n getBufferHealthState(\n isPlaying: boolean,\n currentLatencyMs: number\n ): BufferHealthState;\n getRecommendedAdjustment(): number;\n reset(): void;\n}\n"]}
|
package/ios/AudioPipeline.swift
CHANGED
|
@@ -17,6 +17,7 @@ protocol PipelineListener: AnyObject {
|
|
|
17
17
|
func onZombieDetected(stalledMs: Int64)
|
|
18
18
|
func onUnderrun(count: Int)
|
|
19
19
|
func onDrained(turnId: String)
|
|
20
|
+
func onPlaybackStopped(turnId: String)
|
|
20
21
|
func onAudioFocusLost()
|
|
21
22
|
func onAudioFocusResumed()
|
|
22
23
|
func onFrequencyBands(low: Float, mid: Float, high: Float)
|
|
@@ -84,6 +85,10 @@ class AudioPipeline: SharedAudioEngineDelegate {
|
|
|
84
85
|
/// This prevents duplicate chains and stale callbacks from re-entering after a rebuild.
|
|
85
86
|
private var scheduleGeneration: Int = 0
|
|
86
87
|
|
|
88
|
+
/// Pending PlaybackStopped dispatch — cancelled on new turn / disconnect.
|
|
89
|
+
/// Always mutate from the main queue to avoid races with the drain timer.
|
|
90
|
+
private var pendingPlaybackStoppedWork: DispatchWorkItem?
|
|
91
|
+
|
|
87
92
|
// ── Timers ──────────────────────────────────────────────────────────
|
|
88
93
|
private var stateTimer: DispatchSourceTimer?
|
|
89
94
|
private var zombieTimer: DispatchSourceTimer?
|
|
@@ -122,7 +127,7 @@ class AudioPipeline: SharedAudioEngineDelegate {
|
|
|
122
127
|
// Connect / Disconnect
|
|
123
128
|
// ════════════════════════════════════════════════════════════════════
|
|
124
129
|
|
|
125
|
-
func connect() {
|
|
130
|
+
func connect() throws {
|
|
126
131
|
guard !running else {
|
|
127
132
|
Logger.debug("[\(AudioPipeline.TAG)] connect() called while already running — ignoring")
|
|
128
133
|
return
|
|
@@ -197,12 +202,17 @@ class AudioPipeline: SharedAudioEngineDelegate {
|
|
|
197
202
|
} catch {
|
|
198
203
|
Logger.debug("[\(AudioPipeline.TAG)] connect() failed: \(error)")
|
|
199
204
|
setState(.error)
|
|
200
|
-
listener?.onError(code: "CONNECT_FAILED", message: error.localizedDescription)
|
|
201
205
|
disconnect()
|
|
206
|
+
throw error
|
|
202
207
|
}
|
|
203
208
|
}
|
|
204
209
|
|
|
205
210
|
func disconnect() {
|
|
211
|
+
// Cancel any pending PlaybackStopped before tearing down.
|
|
212
|
+
// DispatchWorkItem.cancel is thread-safe; we may be on any queue here.
|
|
213
|
+
pendingPlaybackStoppedWork?.cancel()
|
|
214
|
+
pendingPlaybackStoppedWork = nil
|
|
215
|
+
|
|
206
216
|
running = false
|
|
207
217
|
// Invalidate all in-flight completion handlers before detaching.
|
|
208
218
|
scheduleGeneration += 1
|
|
@@ -365,6 +375,9 @@ class AudioPipeline: SharedAudioEngineDelegate {
|
|
|
365
375
|
lastReportedUnderrunCount = 0
|
|
366
376
|
setState(.streaming)
|
|
367
377
|
frequencyBandAnalyzer?.reset()
|
|
378
|
+
DispatchQueue.main.async { [weak self] in
|
|
379
|
+
self?.cancelPendingPlaybackStopped()
|
|
380
|
+
}
|
|
368
381
|
}
|
|
369
382
|
|
|
370
383
|
// ── Decode base64 → PCM shorts ──────────────────────────────────
|
|
@@ -407,6 +420,9 @@ class AudioPipeline: SharedAudioEngineDelegate {
|
|
|
407
420
|
lastReportedUnderrunCount = 0
|
|
408
421
|
setState(.idle)
|
|
409
422
|
frequencyBandAnalyzer?.reset()
|
|
423
|
+
DispatchQueue.main.async { [weak self] in
|
|
424
|
+
self?.cancelPendingPlaybackStopped()
|
|
425
|
+
}
|
|
410
426
|
}
|
|
411
427
|
|
|
412
428
|
// ════════════════════════════════════════════════════════════════════
|
|
@@ -433,6 +449,18 @@ class AudioPipeline: SharedAudioEngineDelegate {
|
|
|
433
449
|
]
|
|
434
450
|
}
|
|
435
451
|
|
|
452
|
+
/// Current platform output latency in milliseconds.
|
|
453
|
+
///
|
|
454
|
+
/// Reads `AVAudioSession.sharedInstance().outputLatency`. The value
|
|
455
|
+
/// reflects total HW output latency to the speaker and changes on
|
|
456
|
+
/// audio route changes (built-in vs. Bluetooth vs. wired).
|
|
457
|
+
///
|
|
458
|
+
/// Returns 0 if not running.
|
|
459
|
+
func outputLatencyMs() -> Double {
|
|
460
|
+
guard running else { return 0 }
|
|
461
|
+
return AVAudioSession.sharedInstance().outputLatency * 1000.0
|
|
462
|
+
}
|
|
463
|
+
|
|
436
464
|
// ════════════════════════════════════════════════════════════════════
|
|
437
465
|
// Scheduling loop
|
|
438
466
|
// ════════════════════════════════════════════════════════════════════
|
|
@@ -548,11 +576,48 @@ class AudioPipeline: SharedAudioEngineDelegate {
|
|
|
548
576
|
if buf.isDrained() && currentState == .draining {
|
|
549
577
|
if let tid = turnId {
|
|
550
578
|
listener?.onDrained(turnId: tid)
|
|
579
|
+
schedulePlaybackStopped(turnId: tid)
|
|
551
580
|
}
|
|
552
581
|
setState(.idle)
|
|
553
582
|
}
|
|
554
583
|
}
|
|
555
584
|
|
|
585
|
+
/// Schedule a `PlaybackStopped` event approximately `outputLatencyMs`
|
|
586
|
+
/// after `Drained`. Cancels any previously pending dispatch.
|
|
587
|
+
///
|
|
588
|
+
/// Approximation note: at drain detection, up to `PRE_SCHEDULE_COUNT`
|
|
589
|
+
/// pre-scheduled buffers may still be in the player node's chain — but
|
|
590
|
+
/// they read silence from the empty jitter buffer, so the last audible
|
|
591
|
+
/// sample stops emitting roughly `outputLatency` after this point.
|
|
592
|
+
private func schedulePlaybackStopped(turnId: String) {
|
|
593
|
+
pendingPlaybackStoppedWork?.cancel()
|
|
594
|
+
pendingPlaybackStoppedWork = nil
|
|
595
|
+
|
|
596
|
+
let latencyMs = outputLatencyMs()
|
|
597
|
+
let work = DispatchWorkItem { [weak self] in
|
|
598
|
+
guard let self = self else { return }
|
|
599
|
+
self.pendingPlaybackStoppedWork = nil
|
|
600
|
+
self.listener?.onPlaybackStopped(turnId: turnId)
|
|
601
|
+
}
|
|
602
|
+
pendingPlaybackStoppedWork = work
|
|
603
|
+
|
|
604
|
+
if latencyMs > 0 {
|
|
605
|
+
DispatchQueue.main.asyncAfter(
|
|
606
|
+
deadline: .now() + .milliseconds(Int(latencyMs.rounded())),
|
|
607
|
+
execute: work
|
|
608
|
+
)
|
|
609
|
+
} else {
|
|
610
|
+
DispatchQueue.main.async(execute: work)
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/// Cancel any pending PlaybackStopped dispatch. Call from turn-boundary
|
|
615
|
+
/// transitions (new pushAudio, invalidateTurn) and disconnect.
|
|
616
|
+
private func cancelPendingPlaybackStopped() {
|
|
617
|
+
pendingPlaybackStoppedWork?.cancel()
|
|
618
|
+
pendingPlaybackStoppedWork = nil
|
|
619
|
+
}
|
|
620
|
+
|
|
556
621
|
// ════════════════════════════════════════════════════════════════════
|
|
557
622
|
// Zombie detection
|
|
558
623
|
// ════════════════════════════════════════════════════════════════════
|
|
@@ -48,6 +48,7 @@ public class ExpoPlayAudioStreamModule: Module, MicrophoneDataDelegate, Pipeline
|
|
|
48
48
|
PipelineIntegration.EVENT_ZOMBIE_DETECTED,
|
|
49
49
|
PipelineIntegration.EVENT_UNDERRUN,
|
|
50
50
|
PipelineIntegration.EVENT_DRAINED,
|
|
51
|
+
PipelineIntegration.EVENT_PLAYBACK_STOPPED,
|
|
51
52
|
PipelineIntegration.EVENT_AUDIO_FOCUS_LOST,
|
|
52
53
|
PipelineIntegration.EVENT_AUDIO_FOCUS_RESUMED,
|
|
53
54
|
PipelineIntegration.EVENT_FREQUENCY_BANDS,
|
|
@@ -158,10 +159,36 @@ public class ExpoPlayAudioStreamModule: Module, MicrophoneDataDelegate, Pipeline
|
|
|
158
159
|
|
|
159
160
|
AsyncFunction("connectPipeline") { (options: [String: Any], promise: Promise) in
|
|
160
161
|
do {
|
|
162
|
+
// Always ensure the session is set up (no-op if already initialized).
|
|
163
|
+
// The one-time guard inside ensureAudioSessionInitialized covers
|
|
164
|
+
// the mic-only path; we re-apply the category below every connect
|
|
165
|
+
// because audioMode may change between connects.
|
|
161
166
|
if !self.isAudioSessionInitialized {
|
|
162
167
|
try self.ensureAudioSessionInitialized()
|
|
163
168
|
}
|
|
164
169
|
|
|
170
|
+
// Parse audioMode (default: "mixWithOthers")
|
|
171
|
+
let audioModeString = options["audioMode"] as? String ?? "mixWithOthers"
|
|
172
|
+
var categoryOptions: AVAudioSession.CategoryOptions =
|
|
173
|
+
[.defaultToSpeaker, .allowBluetooth, .allowBluetoothA2DP]
|
|
174
|
+
switch audioModeString {
|
|
175
|
+
case "mixWithOthers":
|
|
176
|
+
categoryOptions.insert(.mixWithOthers)
|
|
177
|
+
case "duckOthers":
|
|
178
|
+
categoryOptions.insert(.duckOthers)
|
|
179
|
+
case "doNotMix":
|
|
180
|
+
break // no additional option
|
|
181
|
+
default:
|
|
182
|
+
categoryOptions.insert(.mixWithOthers)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Reconfigure the session category with the right mix options.
|
|
186
|
+
// Runtime category changes are supported on iOS.
|
|
187
|
+
let audioSession = AVAudioSession.sharedInstance()
|
|
188
|
+
try audioSession.setCategory(
|
|
189
|
+
.playAndRecord, mode: .videoChat, options: categoryOptions)
|
|
190
|
+
try audioSession.setActive(true)
|
|
191
|
+
|
|
165
192
|
// Parse playback mode from options to configure shared engine.
|
|
166
193
|
// Always use VP — this library is meant for mic+speaker combos.
|
|
167
194
|
let playbackModeString = options["playbackMode"] as? String ?? "conversation"
|
|
@@ -183,6 +210,15 @@ public class ExpoPlayAudioStreamModule: Module, MicrophoneDataDelegate, Pipeline
|
|
|
183
210
|
|
|
184
211
|
promise.resolve(result)
|
|
185
212
|
} catch {
|
|
213
|
+
// Reset session + engine state so a subsequent connect can recover.
|
|
214
|
+
// Without this, a partial failure (e.g. setActive denial after the
|
|
215
|
+
// iOS local-network permission prompt) leaves the session stuck and
|
|
216
|
+
// every retry fails the same way.
|
|
217
|
+
self._pipelineIntegration?.removeAsDelegate(from: self.sharedAudioEngine)
|
|
218
|
+
self._pipelineIntegration?.disconnect()
|
|
219
|
+
self.sharedAudioEngine.teardown()
|
|
220
|
+
try? AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
|
|
221
|
+
self.isAudioSessionInitialized = false
|
|
186
222
|
promise.reject("PIPELINE_CONNECT_ERROR", error.localizedDescription)
|
|
187
223
|
}
|
|
188
224
|
}
|
|
@@ -222,6 +258,10 @@ public class ExpoPlayAudioStreamModule: Module, MicrophoneDataDelegate, Pipeline
|
|
|
222
258
|
Function("getPipelineState") { () -> String in
|
|
223
259
|
return self.pipelineIntegration.getState()
|
|
224
260
|
}
|
|
261
|
+
|
|
262
|
+
Function("getPipelineOutputLatencyMs") { () -> Double in
|
|
263
|
+
return self.pipelineIntegration.outputLatencyMs()
|
|
264
|
+
}
|
|
225
265
|
}
|
|
226
266
|
|
|
227
267
|
private func ensureAudioSessionInitialized(settings recordingSettings: RecordingSettings? = nil) throws {
|