@edkimmel/expo-audio-stream 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.
Files changed (75) hide show
  1. package/.eslintrc.js +5 -0
  2. package/.yarnrc.yml +8 -0
  3. package/NATIVE_EVENTS.md +270 -0
  4. package/README.md +289 -0
  5. package/android/build.gradle +92 -0
  6. package/android/src/main/AndroidManifest.xml +4 -0
  7. package/android/src/main/java/expo/modules/audiostream/AudioDataEncoder.kt +178 -0
  8. package/android/src/main/java/expo/modules/audiostream/AudioEffectsManager.kt +107 -0
  9. package/android/src/main/java/expo/modules/audiostream/AudioPlaybackManager.kt +651 -0
  10. package/android/src/main/java/expo/modules/audiostream/AudioRecorderManager.kt +509 -0
  11. package/android/src/main/java/expo/modules/audiostream/Constants.kt +21 -0
  12. package/android/src/main/java/expo/modules/audiostream/EventSender.kt +7 -0
  13. package/android/src/main/java/expo/modules/audiostream/ExpoAudioStreamView.kt +7 -0
  14. package/android/src/main/java/expo/modules/audiostream/ExpoPlayAudioStreamModule.kt +280 -0
  15. package/android/src/main/java/expo/modules/audiostream/PermissionUtils.kt +16 -0
  16. package/android/src/main/java/expo/modules/audiostream/RecordingConfig.kt +60 -0
  17. package/android/src/main/java/expo/modules/audiostream/SoundConfig.kt +46 -0
  18. package/android/src/main/java/expo/modules/audiostream/pipeline/AudioPipeline.kt +685 -0
  19. package/android/src/main/java/expo/modules/audiostream/pipeline/JitterBuffer.kt +227 -0
  20. package/android/src/main/java/expo/modules/audiostream/pipeline/PipelineIntegration.kt +315 -0
  21. package/app.plugin.js +1 -0
  22. package/build/ExpoPlayAudioStreamModule.d.ts +3 -0
  23. package/build/ExpoPlayAudioStreamModule.d.ts.map +1 -0
  24. package/build/ExpoPlayAudioStreamModule.js +5 -0
  25. package/build/ExpoPlayAudioStreamModule.js.map +1 -0
  26. package/build/events.d.ts +36 -0
  27. package/build/events.d.ts.map +1 -0
  28. package/build/events.js +25 -0
  29. package/build/events.js.map +1 -0
  30. package/build/index.d.ts +125 -0
  31. package/build/index.d.ts.map +1 -0
  32. package/build/index.js +222 -0
  33. package/build/index.js.map +1 -0
  34. package/build/pipeline/index.d.ts +81 -0
  35. package/build/pipeline/index.d.ts.map +1 -0
  36. package/build/pipeline/index.js +140 -0
  37. package/build/pipeline/index.js.map +1 -0
  38. package/build/pipeline/types.d.ts +132 -0
  39. package/build/pipeline/types.d.ts.map +1 -0
  40. package/build/pipeline/types.js +5 -0
  41. package/build/pipeline/types.js.map +1 -0
  42. package/build/types.d.ts +221 -0
  43. package/build/types.d.ts.map +1 -0
  44. package/build/types.js +10 -0
  45. package/build/types.js.map +1 -0
  46. package/expo-module.config.json +9 -0
  47. package/ios/AudioPipeline.swift +562 -0
  48. package/ios/AudioUtils.swift +356 -0
  49. package/ios/ExpoPlayAudioStream.podspec +27 -0
  50. package/ios/ExpoPlayAudioStreamModule.swift +436 -0
  51. package/ios/ExpoPlayAudioStreamView.swift +7 -0
  52. package/ios/JitterBuffer.swift +208 -0
  53. package/ios/Logger.swift +7 -0
  54. package/ios/Microphone.swift +221 -0
  55. package/ios/MicrophoneDataDelegate.swift +4 -0
  56. package/ios/PipelineIntegration.swift +214 -0
  57. package/ios/RecordingResult.swift +10 -0
  58. package/ios/RecordingSettings.swift +11 -0
  59. package/ios/SharedAudioEngine.swift +484 -0
  60. package/ios/SoundConfig.swift +45 -0
  61. package/ios/SoundPlayer.swift +408 -0
  62. package/ios/SoundPlayerDelegate.swift +7 -0
  63. package/package.json +49 -0
  64. package/plugin/build/index.d.ts +5 -0
  65. package/plugin/build/index.js +28 -0
  66. package/plugin/src/index.ts +53 -0
  67. package/plugin/tsconfig.json +9 -0
  68. package/plugin/tsconfig.tsbuildinfo +1 -0
  69. package/src/ExpoPlayAudioStreamModule.ts +5 -0
  70. package/src/events.ts +66 -0
  71. package/src/index.ts +359 -0
  72. package/src/pipeline/index.ts +216 -0
  73. package/src/pipeline/types.ts +169 -0
  74. package/src/types.ts +270 -0
  75. package/tsconfig.json +9 -0
@@ -0,0 +1,125 @@
1
+ import type { EventSubscription } from "expo-modules-core";
2
+ type Subscription = EventSubscription;
3
+ import { AudioDataEvent, AudioRecording, RecordingConfig, StartRecordingResult, SoundConfig, PlaybackMode, Encoding, EncodingTypes, PlaybackModes, IAudioBufferConfig, IAudioPlayPayload, IAudioFrame, BufferHealthState, IBufferHealthMetrics, IAudioBufferManager, IFrameProcessor, IQualityMonitor, BufferedStreamConfig, SmartBufferConfig, SmartBufferMode, NetworkConditions } from "./types";
4
+ import { SoundChunkPlayedEventPayload, AudioEvents, DeviceReconnectedReason, DeviceReconnectedEventPayload } from "./events";
5
+ declare const SuspendSoundEventTurnId = "suspend-sound-events";
6
+ export declare class ExpoPlayAudioStream {
7
+ /**
8
+ * Destroys the audio stream module, cleaning up all resources.
9
+ * This should be called when the module is no longer needed.
10
+ * It will reset all internal state and release audio resources.
11
+ */
12
+ static destroy(): void;
13
+ /**
14
+ * @deprecated Use the `Pipeline` class for more efficient audio streaming with better error handling and telemetry.
15
+ * Plays a sound.
16
+ * @param {string} audio - The audio to play.
17
+ * @param {string} turnId - The turn ID.
18
+ * @param {string} [encoding] - The encoding format of the audio data ('pcm_f32le' or 'pcm_s16le').
19
+ * @returns {Promise<void>}
20
+ * @throws {Error} If the sound fails to play.
21
+ */
22
+ static playSound(audio: string, turnId: string, encoding?: Encoding): Promise<void>;
23
+ /**
24
+ * @deprecated Use the `Pipeline` class for more efficient audio streaming with better error handling and telemetry.
25
+ * Stops the currently playing sound.
26
+ * @returns {Promise<void>}
27
+ * @throws {Error} If the sound fails to stop.
28
+ */
29
+ static stopSound(): Promise<void>;
30
+ /**
31
+ * @deprecated Use the `Pipeline` class for more efficient audio streaming with better error handling and telemetry.
32
+ * Clears the sound queue by turn ID.
33
+ * @param {string} turnId - The turn ID.
34
+ * @returns {Promise<void>}
35
+ * @throws {Error} If the sound queue fails to clear.
36
+ */
37
+ static clearSoundQueueByTurnId(turnId: string): Promise<void>;
38
+ /**
39
+ * Starts microphone streaming.
40
+ * @param {RecordingConfig} recordingConfig - The recording configuration.
41
+ * @returns {Promise<{recordingResult: StartRecordingResult, subscription: Subscription}>} A promise that resolves to an object containing the recording result and a subscription to audio events.
42
+ * @throws {Error} If the recording fails to start.
43
+ */
44
+ static startMicrophone(recordingConfig: RecordingConfig): Promise<{
45
+ recordingResult: StartRecordingResult;
46
+ subscription?: Subscription;
47
+ }>;
48
+ /**
49
+ * Stops the current microphone streaming.
50
+ * @returns {Promise<void>}
51
+ * @throws {Error} If the microphone streaming fails to stop.
52
+ */
53
+ static stopMicrophone(): Promise<AudioRecording | null>;
54
+ /**
55
+ * Subscribes to audio events emitted during recording/streaming.
56
+ * @param onMicrophoneStream - Callback function that will be called when audio data is received.
57
+ * The callback receives an AudioDataEvent containing:
58
+ * - data: Base64 encoded audio data at original sample rate
59
+ * - data16kHz: Optional base64 encoded audio data resampled to 16kHz
60
+ * - position: Current position in the audio stream
61
+ * - fileUri: URI of the recording file
62
+ * - eventDataSize: Size of the current audio data chunk
63
+ * - totalSize: Total size of recorded audio so far
64
+ * @returns {Subscription} A subscription object that can be used to unsubscribe from the events
65
+ * @throws {Error} If encoded audio data is missing from the event
66
+ */
67
+ static subscribeToAudioEvents(onMicrophoneStream: (event: AudioDataEvent) => Promise<void>): Subscription;
68
+ /**
69
+ * Subscribes to events emitted when a sound chunk has finished playing.
70
+ * @param onSoundChunkPlayed - Callback function that will be called when a sound chunk is played.
71
+ * The callback receives a SoundChunkPlayedEventPayload indicating if this was the final chunk.
72
+ * @returns {Subscription} A subscription object that can be used to unsubscribe from the events.
73
+ */
74
+ static subscribeToSoundChunkPlayed(onSoundChunkPlayed: (event: SoundChunkPlayedEventPayload) => Promise<void>): Subscription;
75
+ /**
76
+ * Subscribes to events emitted by the audio stream module, for advanced use cases.
77
+ * @param eventName - The name of the event to subscribe to.
78
+ * @param onEvent - Callback function that will be called when the event is emitted.
79
+ * @returns {Subscription} A subscription object that can be used to unsubscribe from the events.
80
+ */
81
+ static subscribe<T extends unknown>(eventName: string, onEvent: (event: T | undefined) => Promise<void>): Subscription;
82
+ /**
83
+ * Sets the sound player configuration.
84
+ * @param {SoundConfig} config - Configuration options for the sound player.
85
+ * @returns {Promise<void>}
86
+ * @throws {Error} If the configuration fails to update.
87
+ */
88
+ static setSoundConfig(config: SoundConfig): Promise<void>;
89
+ /**
90
+ * Prompts the user to select the microphone mode.
91
+ * @returns {Promise<void>}
92
+ * @throws {Error} If the microphone mode fails to prompt.
93
+ */
94
+ static promptMicrophoneModes(): void;
95
+ /**
96
+ * Toggles the silence state of the microphone.
97
+ * @returns {Promise<void>}
98
+ * @throws {Error} If the microphone fails to toggle silence.
99
+ */
100
+ static toggleSilence(isSilent: boolean): void;
101
+ /**
102
+ * Requests microphone permission from the user.
103
+ * @returns {Promise<{granted: boolean, canAskAgain?: boolean, status?: string}>} A promise that resolves to the permission result.
104
+ */
105
+ static requestPermissionsAsync(): Promise<{
106
+ granted: boolean;
107
+ canAskAgain?: boolean;
108
+ status?: string;
109
+ }>;
110
+ /**
111
+ * Gets the current microphone permission status.
112
+ * @returns {Promise<{granted: boolean, canAskAgain?: boolean, status?: string}>} A promise that resolves to the permission status.
113
+ */
114
+ static getPermissionsAsync(): Promise<{
115
+ granted: boolean;
116
+ canAskAgain?: boolean;
117
+ status?: string;
118
+ }>;
119
+ }
120
+ export { AudioDataEvent, SoundChunkPlayedEventPayload, DeviceReconnectedReason, DeviceReconnectedEventPayload, AudioRecording, RecordingConfig, StartRecordingResult, AudioEvents, SuspendSoundEventTurnId, SoundConfig, PlaybackMode, Encoding, EncodingTypes, PlaybackModes, IAudioBufferConfig, IAudioPlayPayload, IAudioFrame, BufferHealthState, IBufferHealthMetrics, IAudioBufferManager, IFrameProcessor, IQualityMonitor, BufferedStreamConfig, SmartBufferConfig, SmartBufferMode, NetworkConditions, };
121
+ export type { EventSubscription } from "expo-modules-core";
122
+ export type { Subscription } from "./events";
123
+ export { Pipeline } from "./pipeline";
124
+ export type { ConnectPipelineOptions, ConnectPipelineResult, PushPipelineAudioOptions, InvalidatePipelineTurnOptions, PipelineState, PipelineEventMap, PipelineEventName, PipelineBufferTelemetry, PipelineTelemetry, PipelineStateChangedEvent, PipelinePlaybackStartedEvent, PipelineErrorEvent, PipelineZombieDetectedEvent, PipelineUnderrunEvent, PipelineDrainedEvent, PipelineAudioFocusLostEvent, PipelineAudioFocusResumedEvent, } from "./pipeline";
125
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/build/index.js ADDED
@@ -0,0 +1,222 @@
1
+ import ExpoPlayAudioStreamModule from "./ExpoPlayAudioStreamModule";
2
+ import { EncodingTypes, PlaybackModes, } from "./types";
3
+ import { addAudioEventListener, addSoundChunkPlayedListener, AudioEvents, subscribeToEvent, } from "./events";
4
+ const SuspendSoundEventTurnId = "suspend-sound-events";
5
+ export class ExpoPlayAudioStream {
6
+ /**
7
+ * Destroys the audio stream module, cleaning up all resources.
8
+ * This should be called when the module is no longer needed.
9
+ * It will reset all internal state and release audio resources.
10
+ */
11
+ static destroy() {
12
+ ExpoPlayAudioStreamModule.destroy();
13
+ }
14
+ /**
15
+ * @deprecated Use the `Pipeline` class for more efficient audio streaming with better error handling and telemetry.
16
+ * Plays a sound.
17
+ * @param {string} audio - The audio to play.
18
+ * @param {string} turnId - The turn ID.
19
+ * @param {string} [encoding] - The encoding format of the audio data ('pcm_f32le' or 'pcm_s16le').
20
+ * @returns {Promise<void>}
21
+ * @throws {Error} If the sound fails to play.
22
+ */
23
+ static async playSound(audio, turnId, encoding) {
24
+ try {
25
+ await ExpoPlayAudioStreamModule.playSound(audio, turnId, encoding ?? EncodingTypes.PCM_S16LE);
26
+ }
27
+ catch (error) {
28
+ console.error(error);
29
+ throw new Error(`Failed to enqueue audio: ${error}`);
30
+ }
31
+ }
32
+ /**
33
+ * @deprecated Use the `Pipeline` class for more efficient audio streaming with better error handling and telemetry.
34
+ * Stops the currently playing sound.
35
+ * @returns {Promise<void>}
36
+ * @throws {Error} If the sound fails to stop.
37
+ */
38
+ static async stopSound() {
39
+ try {
40
+ await ExpoPlayAudioStreamModule.stopSound();
41
+ }
42
+ catch (error) {
43
+ console.error(error);
44
+ throw new Error(`Failed to stop enqueued audio: ${error}`);
45
+ }
46
+ }
47
+ /**
48
+ * @deprecated Use the `Pipeline` class for more efficient audio streaming with better error handling and telemetry.
49
+ * Clears the sound queue by turn ID.
50
+ * @param {string} turnId - The turn ID.
51
+ * @returns {Promise<void>}
52
+ * @throws {Error} If the sound queue fails to clear.
53
+ */
54
+ static async clearSoundQueueByTurnId(turnId) {
55
+ try {
56
+ await ExpoPlayAudioStreamModule.clearSoundQueueByTurnId(turnId);
57
+ }
58
+ catch (error) {
59
+ console.error(error);
60
+ throw new Error(`Failed to clear sound queue: ${error}`);
61
+ }
62
+ }
63
+ /**
64
+ * Starts microphone streaming.
65
+ * @param {RecordingConfig} recordingConfig - The recording configuration.
66
+ * @returns {Promise<{recordingResult: StartRecordingResult, subscription: Subscription}>} A promise that resolves to an object containing the recording result and a subscription to audio events.
67
+ * @throws {Error} If the recording fails to start.
68
+ */
69
+ static async startMicrophone(recordingConfig) {
70
+ let subscription;
71
+ try {
72
+ const { onAudioStream, ...options } = recordingConfig;
73
+ if (onAudioStream && typeof onAudioStream == "function") {
74
+ subscription = addAudioEventListener(async (event) => {
75
+ const { fileUri, deltaSize, totalSize, position, encoded, soundLevel, } = event;
76
+ if (!encoded) {
77
+ console.error(`[ExpoPlayAudioStream] Encoded audio data is missing`);
78
+ throw new Error("Encoded audio data is missing");
79
+ }
80
+ onAudioStream?.({
81
+ data: encoded,
82
+ position,
83
+ fileUri,
84
+ eventDataSize: deltaSize,
85
+ totalSize,
86
+ soundLevel,
87
+ });
88
+ });
89
+ }
90
+ const result = await ExpoPlayAudioStreamModule.startMicrophone(options);
91
+ return { recordingResult: result, subscription };
92
+ }
93
+ catch (error) {
94
+ console.error(error);
95
+ subscription?.remove();
96
+ throw new Error(`Failed to start recording: ${error}`);
97
+ }
98
+ }
99
+ /**
100
+ * Stops the current microphone streaming.
101
+ * @returns {Promise<void>}
102
+ * @throws {Error} If the microphone streaming fails to stop.
103
+ */
104
+ static async stopMicrophone() {
105
+ try {
106
+ return await ExpoPlayAudioStreamModule.stopMicrophone();
107
+ }
108
+ catch (error) {
109
+ console.error(error);
110
+ throw new Error(`Failed to stop mic stream: ${error}`);
111
+ }
112
+ }
113
+ /**
114
+ * Subscribes to audio events emitted during recording/streaming.
115
+ * @param onMicrophoneStream - Callback function that will be called when audio data is received.
116
+ * The callback receives an AudioDataEvent containing:
117
+ * - data: Base64 encoded audio data at original sample rate
118
+ * - data16kHz: Optional base64 encoded audio data resampled to 16kHz
119
+ * - position: Current position in the audio stream
120
+ * - fileUri: URI of the recording file
121
+ * - eventDataSize: Size of the current audio data chunk
122
+ * - totalSize: Total size of recorded audio so far
123
+ * @returns {Subscription} A subscription object that can be used to unsubscribe from the events
124
+ * @throws {Error} If encoded audio data is missing from the event
125
+ */
126
+ static subscribeToAudioEvents(onMicrophoneStream) {
127
+ return addAudioEventListener(async (event) => {
128
+ const { fileUri, deltaSize, totalSize, position, encoded, soundLevel } = event;
129
+ if (!encoded) {
130
+ console.error(`[ExpoPlayAudioStream] Encoded audio data is missing`);
131
+ throw new Error("Encoded audio data is missing");
132
+ }
133
+ onMicrophoneStream?.({
134
+ data: encoded,
135
+ position,
136
+ fileUri,
137
+ eventDataSize: deltaSize,
138
+ totalSize,
139
+ soundLevel,
140
+ });
141
+ });
142
+ }
143
+ /**
144
+ * Subscribes to events emitted when a sound chunk has finished playing.
145
+ * @param onSoundChunkPlayed - Callback function that will be called when a sound chunk is played.
146
+ * The callback receives a SoundChunkPlayedEventPayload indicating if this was the final chunk.
147
+ * @returns {Subscription} A subscription object that can be used to unsubscribe from the events.
148
+ */
149
+ static subscribeToSoundChunkPlayed(onSoundChunkPlayed) {
150
+ return addSoundChunkPlayedListener(onSoundChunkPlayed);
151
+ }
152
+ /**
153
+ * Subscribes to events emitted by the audio stream module, for advanced use cases.
154
+ * @param eventName - The name of the event to subscribe to.
155
+ * @param onEvent - Callback function that will be called when the event is emitted.
156
+ * @returns {Subscription} A subscription object that can be used to unsubscribe from the events.
157
+ */
158
+ static subscribe(eventName, onEvent) {
159
+ return subscribeToEvent(eventName, onEvent);
160
+ }
161
+ /**
162
+ * Sets the sound player configuration.
163
+ * @param {SoundConfig} config - Configuration options for the sound player.
164
+ * @returns {Promise<void>}
165
+ * @throws {Error} If the configuration fails to update.
166
+ */
167
+ static async setSoundConfig(config) {
168
+ try {
169
+ await ExpoPlayAudioStreamModule.setSoundConfig(config);
170
+ }
171
+ catch (error) {
172
+ console.error(error);
173
+ throw new Error(`Failed to set sound configuration: ${error}`);
174
+ }
175
+ }
176
+ /**
177
+ * Prompts the user to select the microphone mode.
178
+ * @returns {Promise<void>}
179
+ * @throws {Error} If the microphone mode fails to prompt.
180
+ */
181
+ static promptMicrophoneModes() {
182
+ ExpoPlayAudioStreamModule.promptMicrophoneModes();
183
+ }
184
+ /**
185
+ * Toggles the silence state of the microphone.
186
+ * @returns {Promise<void>}
187
+ * @throws {Error} If the microphone fails to toggle silence.
188
+ */
189
+ static toggleSilence(isSilent) {
190
+ ExpoPlayAudioStreamModule.toggleSilence(isSilent);
191
+ }
192
+ /**
193
+ * Requests microphone permission from the user.
194
+ * @returns {Promise<{granted: boolean, canAskAgain?: boolean, status?: string}>} A promise that resolves to the permission result.
195
+ */
196
+ static async requestPermissionsAsync() {
197
+ try {
198
+ return await ExpoPlayAudioStreamModule.requestPermissionsAsync();
199
+ }
200
+ catch (error) {
201
+ console.error(error);
202
+ throw new Error(`Failed to request permissions: ${error}`);
203
+ }
204
+ }
205
+ /**
206
+ * Gets the current microphone permission status.
207
+ * @returns {Promise<{granted: boolean, canAskAgain?: boolean, status?: string}>} A promise that resolves to the permission status.
208
+ */
209
+ static async getPermissionsAsync() {
210
+ try {
211
+ return await ExpoPlayAudioStreamModule.getPermissionsAsync();
212
+ }
213
+ catch (error) {
214
+ console.error(error);
215
+ throw new Error(`Failed to get permissions: ${error}`);
216
+ }
217
+ }
218
+ }
219
+ export { AudioEvents, SuspendSoundEventTurnId, EncodingTypes, PlaybackModes, };
220
+ // Export native audio pipeline V3
221
+ export { Pipeline } from "./pipeline";
222
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,yBAAyB,MAAM,6BAA6B,CAAC;AAIpE,OAAO,EAQL,aAAa,EACb,aAAa,GAcd,MAAM,SAAS,CAAC;AAEjB,OAAO,EACL,qBAAqB,EACrB,2BAA2B,EAG3B,WAAW,EACX,gBAAgB,GAGjB,MAAM,UAAU,CAAC;AAElB,MAAM,uBAAuB,GAAG,sBAAsB,CAAC;AAEvD,MAAM,OAAO,mBAAmB;IAC9B;;;;OAIG;IACH,MAAM,CAAC,OAAO;QACZ,yBAAyB,CAAC,OAAO,EAAE,CAAC;IACtC,CAAC;IAED;;;;;;;;OAQG;IACH,MAAM,CAAC,KAAK,CAAC,SAAS,CACpB,KAAa,EACb,MAAc,EACd,QAAmB;QAEnB,IAAI,CAAC;YACH,MAAM,yBAAyB,CAAC,SAAS,CACvC,KAAK,EACL,MAAM,EACN,QAAQ,IAAI,aAAa,CAAC,SAAS,CACpC,CAAC;QACJ,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;YACrB,MAAM,IAAI,KAAK,CAAC,4BAA4B,KAAK,EAAE,CAAC,CAAC;QACvD,CAAC;IACH,CAAC;IAED;;;;;OAKG;IACH,MAAM,CAAC,KAAK,CAAC,SAAS;QACpB,IAAI,CAAC;YACH,MAAM,yBAAyB,CAAC,SAAS,EAAE,CAAC;QAC9C,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;YACrB,MAAM,IAAI,KAAK,CAAC,kCAAkC,KAAK,EAAE,CAAC,CAAC;QAC7D,CAAC;IACH,CAAC;IAED;;;;;;OAMG;IACH,MAAM,CAAC,KAAK,CAAC,uBAAuB,CAAC,MAAc;QACjD,IAAI,CAAC;YACH,MAAM,yBAAyB,CAAC,uBAAuB,CAAC,MAAM,CAAC,CAAC;QAClE,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;YACrB,MAAM,IAAI,KAAK,CAAC,gCAAgC,KAAK,EAAE,CAAC,CAAC;QAC3D,CAAC;IACH,CAAC;IAED;;;;;OAKG;IACH,MAAM,CAAC,KAAK,CAAC,eAAe,CAAC,eAAgC;QAI3D,IAAI,YAAsC,CAAC;QAC3C,IAAI,CAAC;YACH,MAAM,EAAE,aAAa,EAAE,GAAG,OAAO,EAAE,GAAG,eAAe,CAAC;YAEtD,IAAI,aAAa,IAAI,OAAO,aAAa,IAAI,UAAU,EAAE,CAAC;gBACxD,YAAY,GAAG,qBAAqB,CAClC,KAAK,EAAE,KAAwB,EAAE,EAAE;oBACjC,MAAM,EACJ,OAAO,EACP,SAAS,EACT,SAAS,EACT,QAAQ,EACR,OAAO,EACP,UAAU,GACX,GAAG,KAAK,CAAC;oBACV,IAAI,CAAC,OAAO,EAAE,CAAC;wBACb,OAAO,CAAC,KAAK,CACX,qDAAqD,CACtD,CAAC;wBACF,MAAM,IAAI,KAAK,CAAC,+BAA+B,CAAC,CAAC;oBACnD,CAAC;oBACD,aAAa,EAAE,CAAC;wBACd,IAAI,EAAE,OAAO;wBACb,QAAQ;wBACR,OAAO;wBACP,aAAa,EAAE,SAAS;wBACxB,SAAS;wBACT,UAAU;qBACX,CAAC,CAAC;gBACL,CAAC,CACF,CAAC;YACJ,CAAC;YAED,MAAM,MAAM,GAAG,MAAM,yBAAyB,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC;YAExE,OAAO,EAAE,eAAe,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC;QACnD,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;YACrB,YAAY,EAAE,MAAM,EAAE,CAAC;YACvB,MAAM,IAAI,KAAK,CAAC,8BAA8B,KAAK,EAAE,CAAC,CAAC;QACzD,CAAC;IACH,CAAC;IAED;;;;OAIG;IACH,MAAM,CAAC,KAAK,CAAC,cAAc;QACzB,IAAI,CAAC;YACH,OAAO,MAAM,yBAAyB,CAAC,cAAc,EAAE,CAAC;QAC1D,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;YACrB,MAAM,IAAI,KAAK,CAAC,8BAA8B,KAAK,EAAE,CAAC,CAAC;QACzD,CAAC;IACH,CAAC;IAED;;;;;;;;;;;;OAYG;IACH,MAAM,CAAC,sBAAsB,CAC3B,kBAA4D;QAE5D,OAAO,qBAAqB,CAAC,KAAK,EAAE,KAAwB,EAAE,EAAE;YAC9D,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE,OAAO,EAAE,UAAU,EAAE,GACpE,KAAK,CAAC;YACR,IAAI,CAAC,OAAO,EAAE,CAAC;gBACb,OAAO,CAAC,KAAK,CAAC,qDAAqD,CAAC,CAAC;gBACrE,MAAM,IAAI,KAAK,CAAC,+BAA+B,CAAC,CAAC;YACnD,CAAC;YACD,kBAAkB,EAAE,CAAC;gBACnB,IAAI,EAAE,OAAO;gBACb,QAAQ;gBACR,OAAO;gBACP,aAAa,EAAE,SAAS;gBACxB,SAAS;gBACT,UAAU;aACX,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;IACL,CAAC;IAED;;;;;OAKG;IACH,MAAM,CAAC,2BAA2B,CAChC,kBAA0E;QAE1E,OAAO,2BAA2B,CAAC,kBAAkB,CAAC,CAAC;IACzD,CAAC;IAED;;;;;OAKG;IACH,MAAM,CAAC,SAAS,CACd,SAAiB,EACjB,OAAgD;QAEhD,OAAO,gBAAgB,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;IAC9C,CAAC;IAED;;;;;OAKG;IACH,MAAM,CAAC,KAAK,CAAC,cAAc,CAAC,MAAmB;QAC7C,IAAI,CAAC;YACH,MAAM,yBAAyB,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;QACzD,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;YACrB,MAAM,IAAI,KAAK,CAAC,sCAAsC,KAAK,EAAE,CAAC,CAAC;QACjE,CAAC;IACH,CAAC;IAED;;;;OAIG;IACH,MAAM,CAAC,qBAAqB;QAC1B,yBAAyB,CAAC,qBAAqB,EAAE,CAAC;IACpD,CAAC;IAED;;;;OAIG;IACH,MAAM,CAAC,aAAa,CAAC,QAAiB;QACpC,yBAAyB,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;IACpD,CAAC;IAED;;;OAGG;IACH,MAAM,CAAC,KAAK,CAAC,uBAAuB;QAKlC,IAAI,CAAC;YACH,OAAO,MAAM,yBAAyB,CAAC,uBAAuB,EAAE,CAAC;QACnE,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;YACrB,MAAM,IAAI,KAAK,CAAC,kCAAkC,KAAK,EAAE,CAAC,CAAC;QAC7D,CAAC;IACH,CAAC;IAED;;;OAGG;IACH,MAAM,CAAC,KAAK,CAAC,mBAAmB;QAK9B,IAAI,CAAC;YACH,OAAO,MAAM,yBAAyB,CAAC,mBAAmB,EAAE,CAAC;QAC/D,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;YACrB,MAAM,IAAI,KAAK,CAAC,8BAA8B,KAAK,EAAE,CAAC,CAAC;QACzD,CAAC;IACH,CAAC;CACF;AAED,OAAO,EAQL,WAAW,EACX,uBAAuB,EAIvB,aAAa,EACb,aAAa,GAcd,CAAC;AAMF,kCAAkC;AAClC,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC","sourcesContent":["import type { EventSubscription } from \"expo-modules-core\";\nimport ExpoPlayAudioStreamModule from \"./ExpoPlayAudioStreamModule\";\n\n// Type alias for backwards compatibility\ntype Subscription = EventSubscription;\nimport {\n AudioDataEvent,\n AudioRecording,\n RecordingConfig,\n StartRecordingResult,\n SoundConfig,\n PlaybackMode,\n Encoding,\n EncodingTypes,\n PlaybackModes,\n // Audio jitter buffer types\n IAudioBufferConfig,\n IAudioPlayPayload,\n IAudioFrame,\n BufferHealthState,\n IBufferHealthMetrics,\n IAudioBufferManager,\n IFrameProcessor,\n IQualityMonitor,\n BufferedStreamConfig,\n SmartBufferConfig,\n SmartBufferMode,\n NetworkConditions,\n} from \"./types\";\n\nimport {\n addAudioEventListener,\n addSoundChunkPlayedListener,\n AudioEventPayload,\n SoundChunkPlayedEventPayload,\n AudioEvents,\n subscribeToEvent,\n DeviceReconnectedReason,\n DeviceReconnectedEventPayload,\n} from \"./events\";\n\nconst SuspendSoundEventTurnId = \"suspend-sound-events\";\n\nexport class ExpoPlayAudioStream {\n /**\n * Destroys the audio stream module, cleaning up all resources.\n * This should be called when the module is no longer needed.\n * It will reset all internal state and release audio resources.\n */\n static destroy() {\n ExpoPlayAudioStreamModule.destroy();\n }\n\n /**\n * @deprecated Use the `Pipeline` class for more efficient audio streaming with better error handling and telemetry.\n * Plays a sound.\n * @param {string} audio - The audio to play.\n * @param {string} turnId - The turn ID.\n * @param {string} [encoding] - The encoding format of the audio data ('pcm_f32le' or 'pcm_s16le').\n * @returns {Promise<void>}\n * @throws {Error} If the sound fails to play.\n */\n static async playSound(\n audio: string,\n turnId: string,\n encoding?: Encoding\n ): Promise<void> {\n try {\n await ExpoPlayAudioStreamModule.playSound(\n audio,\n turnId,\n encoding ?? EncodingTypes.PCM_S16LE\n );\n } catch (error) {\n console.error(error);\n throw new Error(`Failed to enqueue audio: ${error}`);\n }\n }\n\n /**\n * @deprecated Use the `Pipeline` class for more efficient audio streaming with better error handling and telemetry.\n * Stops the currently playing sound.\n * @returns {Promise<void>}\n * @throws {Error} If the sound fails to stop.\n */\n static async stopSound(): Promise<void> {\n try {\n await ExpoPlayAudioStreamModule.stopSound();\n } catch (error) {\n console.error(error);\n throw new Error(`Failed to stop enqueued audio: ${error}`);\n }\n }\n\n /**\n * @deprecated Use the `Pipeline` class for more efficient audio streaming with better error handling and telemetry.\n * Clears the sound queue by turn ID.\n * @param {string} turnId - The turn ID.\n * @returns {Promise<void>}\n * @throws {Error} If the sound queue fails to clear.\n */\n static async clearSoundQueueByTurnId(turnId: string): Promise<void> {\n try {\n await ExpoPlayAudioStreamModule.clearSoundQueueByTurnId(turnId);\n } catch (error) {\n console.error(error);\n throw new Error(`Failed to clear sound queue: ${error}`);\n }\n }\n\n /**\n * Starts microphone streaming.\n * @param {RecordingConfig} recordingConfig - The recording configuration.\n * @returns {Promise<{recordingResult: StartRecordingResult, subscription: Subscription}>} A promise that resolves to an object containing the recording result and a subscription to audio events.\n * @throws {Error} If the recording fails to start.\n */\n static async startMicrophone(recordingConfig: RecordingConfig): Promise<{\n recordingResult: StartRecordingResult;\n subscription?: Subscription;\n }> {\n let subscription: Subscription | undefined;\n try {\n const { onAudioStream, ...options } = recordingConfig;\n\n if (onAudioStream && typeof onAudioStream == \"function\") {\n subscription = addAudioEventListener(\n async (event: AudioEventPayload) => {\n const {\n fileUri,\n deltaSize,\n totalSize,\n position,\n encoded,\n soundLevel,\n } = event;\n if (!encoded) {\n console.error(\n `[ExpoPlayAudioStream] Encoded audio data is missing`\n );\n throw new Error(\"Encoded audio data is missing\");\n }\n onAudioStream?.({\n data: encoded,\n position,\n fileUri,\n eventDataSize: deltaSize,\n totalSize,\n soundLevel,\n });\n }\n );\n }\n\n const result = await ExpoPlayAudioStreamModule.startMicrophone(options);\n\n return { recordingResult: result, subscription };\n } catch (error) {\n console.error(error);\n subscription?.remove();\n throw new Error(`Failed to start recording: ${error}`);\n }\n }\n\n /**\n * Stops the current microphone streaming.\n * @returns {Promise<void>}\n * @throws {Error} If the microphone streaming fails to stop.\n */\n static async stopMicrophone(): Promise<AudioRecording | null> {\n try {\n return await ExpoPlayAudioStreamModule.stopMicrophone();\n } catch (error) {\n console.error(error);\n throw new Error(`Failed to stop mic stream: ${error}`);\n }\n }\n\n /**\n * Subscribes to audio events emitted during recording/streaming.\n * @param onMicrophoneStream - Callback function that will be called when audio data is received.\n * The callback receives an AudioDataEvent containing:\n * - data: Base64 encoded audio data at original sample rate\n * - data16kHz: Optional base64 encoded audio data resampled to 16kHz\n * - position: Current position in the audio stream\n * - fileUri: URI of the recording file\n * - eventDataSize: Size of the current audio data chunk\n * - totalSize: Total size of recorded audio so far\n * @returns {Subscription} A subscription object that can be used to unsubscribe from the events\n * @throws {Error} If encoded audio data is missing from the event\n */\n static subscribeToAudioEvents(\n onMicrophoneStream: (event: AudioDataEvent) => Promise<void>\n ): Subscription {\n return addAudioEventListener(async (event: AudioEventPayload) => {\n const { fileUri, deltaSize, totalSize, position, encoded, soundLevel } =\n event;\n if (!encoded) {\n console.error(`[ExpoPlayAudioStream] Encoded audio data is missing`);\n throw new Error(\"Encoded audio data is missing\");\n }\n onMicrophoneStream?.({\n data: encoded,\n position,\n fileUri,\n eventDataSize: deltaSize,\n totalSize,\n soundLevel,\n });\n });\n }\n\n /**\n * Subscribes to events emitted when a sound chunk has finished playing.\n * @param onSoundChunkPlayed - Callback function that will be called when a sound chunk is played.\n * The callback receives a SoundChunkPlayedEventPayload indicating if this was the final chunk.\n * @returns {Subscription} A subscription object that can be used to unsubscribe from the events.\n */\n static subscribeToSoundChunkPlayed(\n onSoundChunkPlayed: (event: SoundChunkPlayedEventPayload) => Promise<void>\n ): Subscription {\n return addSoundChunkPlayedListener(onSoundChunkPlayed);\n }\n\n /**\n * Subscribes to events emitted by the audio stream module, for advanced use cases.\n * @param eventName - The name of the event to subscribe to.\n * @param onEvent - Callback function that will be called when the event is emitted.\n * @returns {Subscription} A subscription object that can be used to unsubscribe from the events.\n */\n static subscribe<T extends unknown>(\n eventName: string,\n onEvent: (event: T | undefined) => Promise<void>\n ): Subscription {\n return subscribeToEvent(eventName, onEvent);\n }\n\n /**\n * Sets the sound player configuration.\n * @param {SoundConfig} config - Configuration options for the sound player.\n * @returns {Promise<void>}\n * @throws {Error} If the configuration fails to update.\n */\n static async setSoundConfig(config: SoundConfig): Promise<void> {\n try {\n await ExpoPlayAudioStreamModule.setSoundConfig(config);\n } catch (error) {\n console.error(error);\n throw new Error(`Failed to set sound configuration: ${error}`);\n }\n }\n\n /**\n * Prompts the user to select the microphone mode.\n * @returns {Promise<void>}\n * @throws {Error} If the microphone mode fails to prompt.\n */\n static promptMicrophoneModes() {\n ExpoPlayAudioStreamModule.promptMicrophoneModes();\n }\n\n /**\n * Toggles the silence state of the microphone.\n * @returns {Promise<void>}\n * @throws {Error} If the microphone fails to toggle silence.\n */\n static toggleSilence(isSilent: boolean) {\n ExpoPlayAudioStreamModule.toggleSilence(isSilent);\n }\n\n /**\n * Requests microphone permission from the user.\n * @returns {Promise<{granted: boolean, canAskAgain?: boolean, status?: string}>} A promise that resolves to the permission result.\n */\n static async requestPermissionsAsync(): Promise<{\n granted: boolean;\n canAskAgain?: boolean;\n status?: string;\n }> {\n try {\n return await ExpoPlayAudioStreamModule.requestPermissionsAsync();\n } catch (error) {\n console.error(error);\n throw new Error(`Failed to request permissions: ${error}`);\n }\n }\n\n /**\n * Gets the current microphone permission status.\n * @returns {Promise<{granted: boolean, canAskAgain?: boolean, status?: string}>} A promise that resolves to the permission status.\n */\n static async getPermissionsAsync(): Promise<{\n granted: boolean;\n canAskAgain?: boolean;\n status?: string;\n }> {\n try {\n return await ExpoPlayAudioStreamModule.getPermissionsAsync();\n } catch (error) {\n console.error(error);\n throw new Error(`Failed to get permissions: ${error}`);\n }\n }\n}\n\nexport {\n AudioDataEvent,\n SoundChunkPlayedEventPayload,\n DeviceReconnectedReason,\n DeviceReconnectedEventPayload,\n AudioRecording,\n RecordingConfig,\n StartRecordingResult,\n AudioEvents,\n SuspendSoundEventTurnId,\n SoundConfig,\n PlaybackMode,\n Encoding,\n EncodingTypes,\n PlaybackModes,\n // Audio jitter buffer types\n IAudioBufferConfig,\n IAudioPlayPayload,\n IAudioFrame,\n BufferHealthState,\n IBufferHealthMetrics,\n IAudioBufferManager,\n IFrameProcessor,\n IQualityMonitor,\n BufferedStreamConfig,\n SmartBufferConfig,\n SmartBufferMode,\n NetworkConditions,\n};\n\n// Re-export Subscription type for backwards compatibility\nexport type { EventSubscription } from \"expo-modules-core\";\nexport type { Subscription } from \"./events\";\n\n// Export native audio pipeline V3\nexport { Pipeline } from \"./pipeline\";\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 \"./pipeline\";\n"]}
@@ -0,0 +1,81 @@
1
+ import type { EventSubscription } from 'expo-modules-core';
2
+ import type { ConnectPipelineOptions, ConnectPipelineResult, PushPipelineAudioOptions, InvalidatePipelineTurnOptions, PipelineState, PipelineEventMap, PipelineEventName, PipelineTelemetry } from './types';
3
+ export declare class Pipeline {
4
+ /**
5
+ * Connect the native audio pipeline.
6
+ *
7
+ * Creates an AudioTrack (buffer size from device HAL), jitter buffer, and
8
+ * MAX_PRIORITY write thread. Config is immutable per session — disconnect
9
+ * and reconnect to change sample rate.
10
+ */
11
+ static connect(options?: ConnectPipelineOptions): Promise<ConnectPipelineResult>;
12
+ /**
13
+ * Disconnect the pipeline. Tears down AudioTrack, write thread, audio
14
+ * focus, volume guard, and zombie detection.
15
+ */
16
+ static disconnect(): Promise<void>;
17
+ /**
18
+ * Push base64-encoded PCM16 audio into the jitter buffer (async).
19
+ *
20
+ * Use this when you need error propagation via Promise rejection.
21
+ * For the hot path (e.g., inside a WebSocket message handler), prefer
22
+ * [pushAudioSync] which avoids Promise overhead.
23
+ */
24
+ static pushAudio(options: PushPipelineAudioOptions): Promise<void>;
25
+ /**
26
+ * Push base64-encoded PCM16 audio synchronously (no Promise overhead).
27
+ *
28
+ * Designed for the hot path — call this from your WebSocket onmessage
29
+ * handler for minimum latency. Returns `true` on success, `false` on
30
+ * failure (errors are also reported via PipelineError events).
31
+ */
32
+ static pushAudioSync(options: PushPipelineAudioOptions): boolean;
33
+ /**
34
+ * Invalidate the current turn. Resets the jitter buffer so stale audio
35
+ * from the old turn is discarded immediately.
36
+ */
37
+ static invalidateTurn(options: InvalidatePipelineTurnOptions): Promise<void>;
38
+ /** Get the current pipeline state synchronously. */
39
+ static getState(): PipelineState;
40
+ /** Get a telemetry snapshot (buffer levels, counters, etc.). */
41
+ static getTelemetry(): PipelineTelemetry;
42
+ /**
43
+ * Subscribe to a specific pipeline event with full type safety.
44
+ *
45
+ * @example
46
+ * ```ts
47
+ * const sub = Pipeline.subscribe('PipelineStateChanged', async (e) => {
48
+ * console.log('State:', e.state);
49
+ * });
50
+ * // Later:
51
+ * sub.remove();
52
+ * ```
53
+ */
54
+ static subscribe<K extends PipelineEventName>(eventName: K, listener: (event: PipelineEventMap[K]) => Promise<void> | void): EventSubscription;
55
+ /**
56
+ * Convenience: subscribe to both PipelineError and PipelineZombieDetected.
57
+ *
58
+ * Useful for a single error handler that covers fatal and near-fatal
59
+ * conditions. The callback receives a normalized `{ code, message }`.
60
+ */
61
+ static onError(listener: (error: {
62
+ code: string;
63
+ message: string;
64
+ }) => void): {
65
+ remove: () => void;
66
+ };
67
+ /**
68
+ * Convenience: subscribe to audio focus loss and resumption events.
69
+ *
70
+ * During focus loss the pipeline writes silence instead of real audio.
71
+ * The caller should typically invalidateTurn + re-request audio from the
72
+ * AI backend on focus regain.
73
+ */
74
+ static onAudioFocus(listener: (event: {
75
+ focused: boolean;
76
+ }) => void): {
77
+ remove: () => void;
78
+ };
79
+ }
80
+ export type { ConnectPipelineOptions, ConnectPipelineResult, PushPipelineAudioOptions, InvalidatePipelineTurnOptions, PipelineState, PipelineEventMap, PipelineEventName, PipelineBufferTelemetry, PipelineTelemetry, PipelineStateChangedEvent, PipelinePlaybackStartedEvent, PipelineErrorEvent, PipelineZombieDetectedEvent, PipelineUnderrunEvent, PipelineDrainedEvent, PipelineAudioFocusLostEvent, PipelineAudioFocusResumedEvent, } from './types';
81
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/pipeline/index.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAI3D,OAAO,KAAK,EACV,sBAAsB,EACtB,qBAAqB,EACrB,wBAAwB,EACxB,6BAA6B,EAC7B,aAAa,EACb,gBAAgB,EAChB,iBAAiB,EACjB,iBAAiB,EAClB,MAAM,SAAS,CAAC;AAEjB,qBAAa,QAAQ;IAKnB;;;;;;OAMG;WACU,OAAO,CAClB,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,qBAAqB,CAAC;IAIjC;;;OAGG;WACU,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAQxC;;;;;;OAMG;WACU,SAAS,CAAC,OAAO,EAAE,wBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC;IAIxE;;;;;;OAMG;IACH,MAAM,CAAC,aAAa,CAAC,OAAO,EAAE,wBAAwB,GAAG,OAAO;IAQhE;;;OAGG;WACU,cAAc,CACzB,OAAO,EAAE,6BAA6B,GACrC,OAAO,CAAC,IAAI,CAAC;IAQhB,oDAAoD;IACpD,MAAM,CAAC,QAAQ,IAAI,aAAa;IAIhC,gEAAgE;IAChE,MAAM,CAAC,YAAY,IAAI,iBAAiB;IAQxC;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,SAAS,CAAC,CAAC,SAAS,iBAAiB,EAC1C,SAAS,EAAE,CAAC,EACZ,QAAQ,EAAE,CAAC,KAAK,EAAE,gBAAgB,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,GAC7D,iBAAiB;IAWpB;;;;;OAKG;IACH,MAAM,CAAC,OAAO,CACZ,QAAQ,EAAE,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,GAC3D;QAAE,MAAM,EAAE,MAAM,IAAI,CAAA;KAAE;IAuBzB;;;;;;OAMG;IACH,MAAM,CAAC,YAAY,CACjB,QAAQ,EAAE,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,GAC9C;QAAE,MAAM,EAAE,MAAM,IAAI,CAAA;KAAE;CAmB1B;AAGD,YAAY,EACV,sBAAsB,EACtB,qBAAqB,EACrB,wBAAwB,EACxB,6BAA6B,EAC7B,aAAa,EACb,gBAAgB,EAChB,iBAAiB,EACjB,uBAAuB,EACvB,iBAAiB,EACjB,yBAAyB,EACzB,4BAA4B,EAC5B,kBAAkB,EAClB,2BAA2B,EAC3B,qBAAqB,EACrB,oBAAoB,EACpB,2BAA2B,EAC3B,8BAA8B,GAC/B,MAAM,SAAS,CAAC"}
@@ -0,0 +1,140 @@
1
+ // ────────────────────────────────────────────────────────────────────────────
2
+ // Native Audio Pipeline — V3 TypeScript Wrapper
3
+ // ────────────────────────────────────────────────────────────────────────────
4
+ //
5
+ // Thin wrapper over the existing ExpoPlayAudioStreamModule (not a new native
6
+ // module). Uses static methods matching the existing codebase pattern.
7
+ //
8
+ // Hot path: pushAudioSync() — synchronous Function call, no Promise overhead.
9
+ // Cold path: pushAudio() — async with error propagation via Promise.
10
+ import ExpoPlayAudioStreamModule from '../ExpoPlayAudioStreamModule';
11
+ import { subscribeToEvent } from '../events';
12
+ export class Pipeline {
13
+ // ════════════════════════════════════════════════════════════════════════
14
+ // Lifecycle
15
+ // ════════════════════════════════════════════════════════════════════════
16
+ /**
17
+ * Connect the native audio pipeline.
18
+ *
19
+ * Creates an AudioTrack (buffer size from device HAL), jitter buffer, and
20
+ * MAX_PRIORITY write thread. Config is immutable per session — disconnect
21
+ * and reconnect to change sample rate.
22
+ */
23
+ static async connect(options = {}) {
24
+ return await ExpoPlayAudioStreamModule.connectPipeline(options);
25
+ }
26
+ /**
27
+ * Disconnect the pipeline. Tears down AudioTrack, write thread, audio
28
+ * focus, volume guard, and zombie detection.
29
+ */
30
+ static async disconnect() {
31
+ return await ExpoPlayAudioStreamModule.disconnectPipeline();
32
+ }
33
+ // ════════════════════════════════════════════════════════════════════════
34
+ // Push audio
35
+ // ════════════════════════════════════════════════════════════════════════
36
+ /**
37
+ * Push base64-encoded PCM16 audio into the jitter buffer (async).
38
+ *
39
+ * Use this when you need error propagation via Promise rejection.
40
+ * For the hot path (e.g., inside a WebSocket message handler), prefer
41
+ * [pushAudioSync] which avoids Promise overhead.
42
+ */
43
+ static async pushAudio(options) {
44
+ return await ExpoPlayAudioStreamModule.pushPipelineAudio(options);
45
+ }
46
+ /**
47
+ * Push base64-encoded PCM16 audio synchronously (no Promise overhead).
48
+ *
49
+ * Designed for the hot path — call this from your WebSocket onmessage
50
+ * handler for minimum latency. Returns `true` on success, `false` on
51
+ * failure (errors are also reported via PipelineError events).
52
+ */
53
+ static pushAudioSync(options) {
54
+ return ExpoPlayAudioStreamModule.pushPipelineAudioSync(options);
55
+ }
56
+ // ════════════════════════════════════════════════════════════════════════
57
+ // Turn management
58
+ // ════════════════════════════════════════════════════════════════════════
59
+ /**
60
+ * Invalidate the current turn. Resets the jitter buffer so stale audio
61
+ * from the old turn is discarded immediately.
62
+ */
63
+ static async invalidateTurn(options) {
64
+ return await ExpoPlayAudioStreamModule.invalidatePipelineTurn(options);
65
+ }
66
+ // ════════════════════════════════════════════════════════════════════════
67
+ // State & Telemetry
68
+ // ════════════════════════════════════════════════════════════════════════
69
+ /** Get the current pipeline state synchronously. */
70
+ static getState() {
71
+ return ExpoPlayAudioStreamModule.getPipelineState();
72
+ }
73
+ /** Get a telemetry snapshot (buffer levels, counters, etc.). */
74
+ static getTelemetry() {
75
+ return ExpoPlayAudioStreamModule.getPipelineTelemetry();
76
+ }
77
+ // ════════════════════════════════════════════════════════════════════════
78
+ // Event subscriptions
79
+ // ════════════════════════════════════════════════════════════════════════
80
+ /**
81
+ * Subscribe to a specific pipeline event with full type safety.
82
+ *
83
+ * @example
84
+ * ```ts
85
+ * const sub = Pipeline.subscribe('PipelineStateChanged', async (e) => {
86
+ * console.log('State:', e.state);
87
+ * });
88
+ * // Later:
89
+ * sub.remove();
90
+ * ```
91
+ */
92
+ static subscribe(eventName, listener) {
93
+ return subscribeToEvent(eventName, async (event) => {
94
+ if (event !== undefined) {
95
+ await listener(event);
96
+ }
97
+ });
98
+ }
99
+ /**
100
+ * Convenience: subscribe to both PipelineError and PipelineZombieDetected.
101
+ *
102
+ * Useful for a single error handler that covers fatal and near-fatal
103
+ * conditions. The callback receives a normalized `{ code, message }`.
104
+ */
105
+ static onError(listener) {
106
+ const subs = [];
107
+ subs.push(Pipeline.subscribe('PipelineError', async (e) => {
108
+ listener({ code: e.code, message: e.message });
109
+ }));
110
+ subs.push(Pipeline.subscribe('PipelineZombieDetected', async (e) => {
111
+ listener({
112
+ code: 'ZOMBIE_DETECTED',
113
+ message: `AudioTrack stalled for ${e.stalledMs}ms at head=${e.playbackHead}`,
114
+ });
115
+ }));
116
+ return {
117
+ remove: () => subs.forEach((s) => s.remove()),
118
+ };
119
+ }
120
+ /**
121
+ * Convenience: subscribe to audio focus loss and resumption events.
122
+ *
123
+ * During focus loss the pipeline writes silence instead of real audio.
124
+ * The caller should typically invalidateTurn + re-request audio from the
125
+ * AI backend on focus regain.
126
+ */
127
+ static onAudioFocus(listener) {
128
+ const subs = [];
129
+ subs.push(Pipeline.subscribe('PipelineAudioFocusLost', async () => {
130
+ listener({ focused: false });
131
+ }));
132
+ subs.push(Pipeline.subscribe('PipelineAudioFocusResumed', async () => {
133
+ listener({ focused: true });
134
+ }));
135
+ return {
136
+ remove: () => subs.forEach((s) => s.remove()),
137
+ };
138
+ }
139
+ }
140
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/pipeline/index.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,gDAAgD;AAChD,+EAA+E;AAC/E,EAAE;AACF,6EAA6E;AAC7E,uEAAuE;AACvE,EAAE;AACF,+EAA+E;AAC/E,yEAAyE;AAGzE,OAAO,yBAAyB,MAAM,8BAA8B,CAAC;AACrE,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAa7C,MAAM,OAAO,QAAQ;IACnB,2EAA2E;IAC3E,YAAY;IACZ,2EAA2E;IAE3E;;;;;;OAMG;IACH,MAAM,CAAC,KAAK,CAAC,OAAO,CAClB,UAAkC,EAAE;QAEpC,OAAO,MAAM,yBAAyB,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC;IAClE,CAAC;IAED;;;OAGG;IACH,MAAM,CAAC,KAAK,CAAC,UAAU;QACrB,OAAO,MAAM,yBAAyB,CAAC,kBAAkB,EAAE,CAAC;IAC9D,CAAC;IAED,2EAA2E;IAC3E,aAAa;IACb,2EAA2E;IAE3E;;;;;;OAMG;IACH,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,OAAiC;QACtD,OAAO,MAAM,yBAAyB,CAAC,iBAAiB,CAAC,OAAO,CAAC,CAAC;IACpE,CAAC;IAED;;;;;;OAMG;IACH,MAAM,CAAC,aAAa,CAAC,OAAiC;QACpD,OAAO,yBAAyB,CAAC,qBAAqB,CAAC,OAAO,CAAC,CAAC;IAClE,CAAC;IAED,2EAA2E;IAC3E,kBAAkB;IAClB,2EAA2E;IAE3E;;;OAGG;IACH,MAAM,CAAC,KAAK,CAAC,cAAc,CACzB,OAAsC;QAEtC,OAAO,MAAM,yBAAyB,CAAC,sBAAsB,CAAC,OAAO,CAAC,CAAC;IACzE,CAAC;IAED,2EAA2E;IAC3E,oBAAoB;IACpB,2EAA2E;IAE3E,oDAAoD;IACpD,MAAM,CAAC,QAAQ;QACb,OAAO,yBAAyB,CAAC,gBAAgB,EAAmB,CAAC;IACvE,CAAC;IAED,gEAAgE;IAChE,MAAM,CAAC,YAAY;QACjB,OAAO,yBAAyB,CAAC,oBAAoB,EAAuB,CAAC;IAC/E,CAAC;IAED,2EAA2E;IAC3E,sBAAsB;IACtB,2EAA2E;IAE3E;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,SAAS,CACd,SAAY,EACZ,QAA8D;QAE9D,OAAO,gBAAgB,CACrB,SAAS,EACT,KAAK,EAAE,KAAK,EAAE,EAAE;YACd,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;gBACxB,MAAM,QAAQ,CAAC,KAAK,CAAC,CAAC;YACxB,CAAC;QACH,CAAC,CACF,CAAC;IACJ,CAAC;IAED;;;;;OAKG;IACH,MAAM,CAAC,OAAO,CACZ,QAA4D;QAE5D,MAAM,IAAI,GAAwB,EAAE,CAAC;QAErC,IAAI,CAAC,IAAI,CACP,QAAQ,CAAC,SAAS,CAAC,eAAe,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE;YAC9C,QAAQ,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;QACjD,CAAC,CAAC,CACH,CAAC;QAEF,IAAI,CAAC,IAAI,CACP,QAAQ,CAAC,SAAS,CAAC,wBAAwB,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE;YACvD,QAAQ,CAAC;gBACP,IAAI,EAAE,iBAAiB;gBACvB,OAAO,EAAE,0BAA0B,CAAC,CAAC,SAAS,cAAc,CAAC,CAAC,YAAY,EAAE;aAC7E,CAAC,CAAC;QACL,CAAC,CAAC,CACH,CAAC;QAEF,OAAO;YACL,MAAM,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;SAC9C,CAAC;IACJ,CAAC;IAED;;;;;;OAMG;IACH,MAAM,CAAC,YAAY,CACjB,QAA+C;QAE/C,MAAM,IAAI,GAAwB,EAAE,CAAC;QAErC,IAAI,CAAC,IAAI,CACP,QAAQ,CAAC,SAAS,CAAC,wBAAwB,EAAE,KAAK,IAAI,EAAE;YACtD,QAAQ,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;QAC/B,CAAC,CAAC,CACH,CAAC;QAEF,IAAI,CAAC,IAAI,CACP,QAAQ,CAAC,SAAS,CAAC,2BAA2B,EAAE,KAAK,IAAI,EAAE;YACzD,QAAQ,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;QAC9B,CAAC,CAAC,CACH,CAAC;QAEF,OAAO;YACL,MAAM,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;SAC9C,CAAC;IACJ,CAAC;CACF","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"]}