@edkimmel/expo-audio-stream 0.4.1 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -0
- package/android/src/main/java/expo/modules/audiostream/pipeline/AudioPipeline.kt +60 -8
- package/android/src/main/java/expo/modules/audiostream/pipeline/PipelineIntegration.kt +2 -0
- package/build/pipeline/types.d.ts +20 -0
- package/build/pipeline/types.d.ts.map +1 -1
- package/build/pipeline/types.js.map +1 -1
- package/ios/ExpoPlayAudioStreamModule.swift +26 -0
- package/package.json +1 -1
- package/src/pipeline/types.ts +21 -0
package/README.md
CHANGED
|
@@ -79,6 +79,7 @@ const result = await Pipeline.connect({
|
|
|
79
79
|
channelCount: 1,
|
|
80
80
|
targetBufferMs: 80,
|
|
81
81
|
frequencyBandIntervalMs: 100, // optional: emit frequency bands every 100ms
|
|
82
|
+
audioMode: "mixWithOthers", // coexist with other apps (default)
|
|
82
83
|
});
|
|
83
84
|
|
|
84
85
|
// Subscribe to events
|
|
@@ -226,11 +227,23 @@ interface ConnectPipelineOptions {
|
|
|
226
227
|
sampleRate?: number; // default 24000
|
|
227
228
|
channelCount?: number; // default 1 (mono)
|
|
228
229
|
targetBufferMs?: number; // ms to buffer before priming gate opens (default 80)
|
|
230
|
+
playbackMode?: "voiceProcessing" | "conversation";
|
|
229
231
|
frequencyBandIntervalMs?: number; // emit PipelineFrequencyBands every N ms (omit to disable)
|
|
230
232
|
frequencyBandConfig?: FrequencyBandConfig; // crossover frequencies (optional)
|
|
233
|
+
audioMode?: "mixWithOthers" | "duckOthers" | "doNotMix"; // default "mixWithOthers"
|
|
231
234
|
}
|
|
232
235
|
```
|
|
233
236
|
|
|
237
|
+
#### `audioMode`
|
|
238
|
+
|
|
239
|
+
Controls how pipeline playback coexists with audio from other apps on the device. Default: `"mixWithOthers"` (matches expo-audio).
|
|
240
|
+
|
|
241
|
+
- **`"mixWithOthers"`** — plays alongside other apps without interrupting them. On Android no audio focus is requested; on iOS the session uses the `.mixWithOthers` category option. Best for sound effects and short clips.
|
|
242
|
+
- **`"duckOthers"`** — requests audio focus with ducking. Other apps lower their volume but keep playing.
|
|
243
|
+
- **`"doNotMix"`** — requests exclusive audio focus. Other apps pause.
|
|
244
|
+
|
|
245
|
+
> **Breaking change:** The default was effectively `"doNotMix"` in prior versions. If you rely on the previous behavior — where connecting the pipeline pauses other apps' audio — pass `audioMode: "doNotMix"` explicitly when calling `Pipeline.connect`.
|
|
246
|
+
|
|
234
247
|
### PushPipelineAudioOptions
|
|
235
248
|
|
|
236
249
|
```typescript
|
|
@@ -58,6 +58,29 @@ interface PipelineListener {
|
|
|
58
58
|
// AudioPipeline
|
|
59
59
|
// ────────────────────────────────────────────────────────────────────────────
|
|
60
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Controls how pipeline playback coexists with audio from other apps.
|
|
63
|
+
* Mirrors the `PipelineAudioMode` TS type.
|
|
64
|
+
*/
|
|
65
|
+
enum class AudioMode {
|
|
66
|
+
/** No focus request — playback mixes freely with other audio. */
|
|
67
|
+
MIX_WITH_OTHERS,
|
|
68
|
+
|
|
69
|
+
/** Request transient focus with ducking — others lower volume but keep playing. */
|
|
70
|
+
DUCK_OTHERS,
|
|
71
|
+
|
|
72
|
+
/** Request exclusive focus — others pause. */
|
|
73
|
+
DO_NOT_MIX;
|
|
74
|
+
|
|
75
|
+
companion object {
|
|
76
|
+
fun fromString(value: String?): AudioMode = when (value) {
|
|
77
|
+
"duckOthers" -> DUCK_OTHERS
|
|
78
|
+
"doNotMix" -> DO_NOT_MIX
|
|
79
|
+
else -> MIX_WITH_OTHERS // default includes null, "mixWithOthers", and unknown
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
61
84
|
/**
|
|
62
85
|
* Core orchestrator for the native audio pipeline.
|
|
63
86
|
*
|
|
@@ -90,6 +113,7 @@ class AudioPipeline(
|
|
|
90
113
|
private val frequencyBandIntervalMs: Int = 100,
|
|
91
114
|
private val lowCrossoverHz: Float = 300f,
|
|
92
115
|
private val highCrossoverHz: Float = 2000f,
|
|
116
|
+
private val audioMode: AudioMode = AudioMode.MIX_WITH_OTHERS,
|
|
93
117
|
private val listener: PipelineListener
|
|
94
118
|
) {
|
|
95
119
|
companion object {
|
|
@@ -550,18 +574,46 @@ class AudioPipeline(
|
|
|
550
574
|
// ════════════════════════════════════════════════════════════════════
|
|
551
575
|
|
|
552
576
|
private fun requestAudioFocus() {
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
577
|
+
when (audioMode) {
|
|
578
|
+
AudioMode.MIX_WITH_OTHERS -> {
|
|
579
|
+
// No focus request — we coexist silently with other apps.
|
|
580
|
+
// Mark as "has focus" so the write loop proceeds unconditionally.
|
|
581
|
+
hasAudioFocus.set(true)
|
|
582
|
+
audioFocusLost.set(false)
|
|
583
|
+
Log.d(TAG, "Audio focus skipped (mixWithOthers)")
|
|
584
|
+
}
|
|
585
|
+
AudioMode.DUCK_OTHERS -> {
|
|
586
|
+
val result = audioManager.requestAudioFocus(
|
|
587
|
+
focusChangeListener,
|
|
588
|
+
AudioManager.STREAM_MUSIC,
|
|
589
|
+
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK
|
|
590
|
+
)
|
|
591
|
+
hasAudioFocus.set(result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED)
|
|
592
|
+
if (!hasAudioFocus.get()) {
|
|
593
|
+
Log.w(TAG, "Audio focus request (duckOthers) denied")
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
AudioMode.DO_NOT_MIX -> {
|
|
597
|
+
val result = audioManager.requestAudioFocus(
|
|
598
|
+
focusChangeListener,
|
|
599
|
+
AudioManager.STREAM_MUSIC,
|
|
600
|
+
AudioManager.AUDIOFOCUS_GAIN
|
|
601
|
+
)
|
|
602
|
+
hasAudioFocus.set(result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED)
|
|
603
|
+
if (!hasAudioFocus.get()) {
|
|
604
|
+
Log.w(TAG, "Audio focus request (doNotMix) denied")
|
|
605
|
+
}
|
|
606
|
+
}
|
|
561
607
|
}
|
|
562
608
|
}
|
|
563
609
|
|
|
564
610
|
private fun abandonAudioFocus() {
|
|
611
|
+
if (audioMode == AudioMode.MIX_WITH_OTHERS) {
|
|
612
|
+
// No focus was ever requested — nothing to abandon.
|
|
613
|
+
hasAudioFocus.set(false)
|
|
614
|
+
audioFocusLost.set(false)
|
|
615
|
+
return
|
|
616
|
+
}
|
|
565
617
|
audioManager.abandonAudioFocus(focusChangeListener)
|
|
566
618
|
hasAudioFocus.set(false)
|
|
567
619
|
audioFocusLost.set(false)
|
|
@@ -121,6 +121,7 @@ class PipelineIntegration(
|
|
|
121
121
|
val bandConfig = options["frequencyBandConfig"] as? Map<*, *>
|
|
122
122
|
val lowCrossoverHz = (bandConfig?.get("lowCrossoverHz") as? Number)?.toFloat() ?: 300f
|
|
123
123
|
val highCrossoverHz = (bandConfig?.get("highCrossoverHz") as? Number)?.toFloat() ?: 2000f
|
|
124
|
+
val audioMode = AudioMode.fromString(options["audioMode"] as? String)
|
|
124
125
|
|
|
125
126
|
pipeline = AudioPipeline(
|
|
126
127
|
context = context,
|
|
@@ -130,6 +131,7 @@ class PipelineIntegration(
|
|
|
130
131
|
frequencyBandIntervalMs = frequencyBandIntervalMs,
|
|
131
132
|
lowCrossoverHz = lowCrossoverHz,
|
|
132
133
|
highCrossoverHz = highCrossoverHz,
|
|
134
|
+
audioMode = audioMode,
|
|
133
135
|
listener = this
|
|
134
136
|
)
|
|
135
137
|
pipeline!!.connect()
|
|
@@ -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 {
|
|
@@ -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/** 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"]}
|
|
@@ -158,10 +158,36 @@ public class ExpoPlayAudioStreamModule: Module, MicrophoneDataDelegate, Pipeline
|
|
|
158
158
|
|
|
159
159
|
AsyncFunction("connectPipeline") { (options: [String: Any], promise: Promise) in
|
|
160
160
|
do {
|
|
161
|
+
// Always ensure the session is set up (no-op if already initialized).
|
|
162
|
+
// The one-time guard inside ensureAudioSessionInitialized covers
|
|
163
|
+
// the mic-only path; we re-apply the category below every connect
|
|
164
|
+
// because audioMode may change between connects.
|
|
161
165
|
if !self.isAudioSessionInitialized {
|
|
162
166
|
try self.ensureAudioSessionInitialized()
|
|
163
167
|
}
|
|
164
168
|
|
|
169
|
+
// Parse audioMode (default: "mixWithOthers")
|
|
170
|
+
let audioModeString = options["audioMode"] as? String ?? "mixWithOthers"
|
|
171
|
+
var categoryOptions: AVAudioSession.CategoryOptions =
|
|
172
|
+
[.defaultToSpeaker, .allowBluetooth, .allowBluetoothA2DP]
|
|
173
|
+
switch audioModeString {
|
|
174
|
+
case "mixWithOthers":
|
|
175
|
+
categoryOptions.insert(.mixWithOthers)
|
|
176
|
+
case "duckOthers":
|
|
177
|
+
categoryOptions.insert(.duckOthers)
|
|
178
|
+
case "doNotMix":
|
|
179
|
+
break // no additional option
|
|
180
|
+
default:
|
|
181
|
+
categoryOptions.insert(.mixWithOthers)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Reconfigure the session category with the right mix options.
|
|
185
|
+
// Runtime category changes are supported on iOS.
|
|
186
|
+
let audioSession = AVAudioSession.sharedInstance()
|
|
187
|
+
try audioSession.setCategory(
|
|
188
|
+
.playAndRecord, mode: .videoChat, options: categoryOptions)
|
|
189
|
+
try audioSession.setActive(true)
|
|
190
|
+
|
|
165
191
|
// Parse playback mode from options to configure shared engine.
|
|
166
192
|
// Always use VP — this library is meant for mic+speaker combos.
|
|
167
193
|
let playbackModeString = options["playbackMode"] as? String ?? "conversation"
|
package/package.json
CHANGED
package/src/pipeline/types.ts
CHANGED
|
@@ -6,6 +6,18 @@ import { PlaybackMode, FrequencyBandConfig, FrequencyBands } from "../types";
|
|
|
6
6
|
|
|
7
7
|
// ── Connect ─────────────────────────────────────────────────────────────────
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* How the pipeline's playback should coexist with other audio on the device.
|
|
11
|
+
*
|
|
12
|
+
* - `'mixWithOthers'` (default): plays alongside other apps without
|
|
13
|
+
* interrupting them. On Android no audio focus is requested. Best for
|
|
14
|
+
* sound effects and short clips.
|
|
15
|
+
* - `'duckOthers'`: requests audio focus with ducking. Other apps lower
|
|
16
|
+
* their volume but keep playing.
|
|
17
|
+
* - `'doNotMix'`: requests exclusive audio focus. Other apps pause.
|
|
18
|
+
*/
|
|
19
|
+
export type PipelineAudioMode = 'mixWithOthers' | 'duckOthers' | 'doNotMix';
|
|
20
|
+
|
|
9
21
|
/** Options passed to `connectPipeline()`. */
|
|
10
22
|
export interface ConnectPipelineOptions {
|
|
11
23
|
/** Sample rate in Hz (default 24000). */
|
|
@@ -25,6 +37,15 @@ export interface ConnectPipelineOptions {
|
|
|
25
37
|
frequencyBandIntervalMs?: number;
|
|
26
38
|
/** Optional frequency band crossover configuration. */
|
|
27
39
|
frequencyBandConfig?: FrequencyBandConfig;
|
|
40
|
+
/**
|
|
41
|
+
* How pipeline playback should coexist with other apps' audio.
|
|
42
|
+
* Default is `'mixWithOthers'` (matches expo-audio).
|
|
43
|
+
*
|
|
44
|
+
* Note: this is a **behavior change** vs. prior versions of this library,
|
|
45
|
+
* which effectively used `'doNotMix'`. Pass `'doNotMix'` explicitly to
|
|
46
|
+
* preserve that old behavior.
|
|
47
|
+
*/
|
|
48
|
+
audioMode?: PipelineAudioMode;
|
|
28
49
|
}
|
|
29
50
|
|
|
30
51
|
/** Result returned from a successful `connectPipeline()` call. */
|