feat: add expo-two-way-audio module to workspace

This commit is contained in:
Mohamed Boudra
2026-03-14 16:46:35 +07:00
parent fc7369384b
commit cad4110e24
74 changed files with 48803 additions and 354 deletions

View File

@@ -20,7 +20,7 @@ From repo root:
```bash
npm run android:development # Debug build
npm run android:production # Release build
npm run android:clean # Clean native project
npm run android:clear # Remove generated Android project
```
Or from `packages/app`:
@@ -34,8 +34,8 @@ APP_VARIANT=development npx expo run:android --variant=debug
APP_VARIANT=production npx expo prebuild --platform android --non-interactive
APP_VARIANT=production npx expo run:android --variant=release
# Clean
npx expo prebuild --platform android --clean --non-interactive
# Clear generated Android project
rm -rf android
```
## Screenshots

3827
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,7 @@
"version": "0.1.27",
"private": true,
"workspaces": [
"packages/expo-two-way-audio",
"packages/server",
"packages/app",
"packages/relay",
@@ -28,6 +29,7 @@
"android:development": "npm run android:development --workspace=@getpaseo/app",
"android:production": "npm run android:production --workspace=@getpaseo/app",
"android:release": "npm run android:production --workspace=@getpaseo/app",
"android:clear": "npm run android:clear --workspace=@getpaseo/app",
"android:clean": "npm run android:clean --workspace=@getpaseo/app",
"ios": "npm run ios --workspace=@getpaseo/app",
"web": "npm run web --workspace=@getpaseo/app",

View File

@@ -100,6 +100,9 @@ export default {
output: "single",
favicon: "./assets/images/favicon.png",
},
autolinking: {
searchPaths: ["../../node_modules", "./node_modules"],
},
plugins: [
"expo-router",
[
@@ -143,6 +146,7 @@ export default {
experiments: {
typedRoutes: true,
reactCompiler: true,
autolinkingModuleResolution: true,
},
extra: {
router: {},

View File

@@ -7,11 +7,10 @@
"start": "expo start",
"reset-project": "node ./scripts/reset-project.js",
"android": "npm run android:development",
"android:clear-autolinking-cache": "node -e \"require('node:fs').rmSync('android/build/generated/autolinking', { recursive: true, force: true })\"",
"android:development": "npm run android:clear-autolinking-cache && APP_VARIANT=development expo prebuild --platform android --non-interactive && APP_VARIANT=development expo run:android --variant=debug",
"android:production": "npm run android:clear-autolinking-cache && APP_VARIANT=production expo prebuild --platform android --non-interactive && APP_VARIANT=production expo run:android --variant=release",
"android:development": "APP_VARIANT=development expo prebuild --platform android --non-interactive && APP_VARIANT=development expo run:android --variant=debug",
"android:production": "APP_VARIANT=production expo prebuild --platform android --non-interactive && APP_VARIANT=production expo run:android --variant=release",
"android:release": "npm run android:production",
"android:clean": "expo prebuild --platform android --clean --non-interactive",
"android:clear": "node -e \"require('node:fs').rmSync('android', { recursive: true, force: true })\"",
"ios": "expo run:ios",
"ios:release": "expo run:ios --configuration Release",
"web": "expo start --web",
@@ -27,7 +26,7 @@
"deploy:web": "npm run build:web && wrangler pages deploy dist --project-name paseo-app"
},
"dependencies": {
"@boudra/expo-two-way-audio": "^0.1.3",
"@getpaseo/expo-two-way-audio": "file:../expo-two-way-audio",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",

View File

@@ -6,11 +6,13 @@ import {
ScrollView,
Alert,
Platform,
Pressable,
} from "react-native";
import { router, useLocalSearchParams } from "expo-router";
import { useFocusEffect } from "@react-navigation/native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
import { Buffer } from "buffer";
import { Sun, Moon, Monitor, Globe, Settings, RotateCw, Trash2 } from "lucide-react-native";
import { useAppSettings, type AppSettings } from "@/hooks/use-settings";
import type { HostProfile, HostConnection } from "@/types/host-connection";
@@ -43,6 +45,8 @@ import { useDesktopAppUpdater } from "@/desktop/updates/use-desktop-app-updater"
import { formatVersionWithPrefix } from "@/desktop/updates/desktop-updates";
import { resolveAppVersion } from "@/utils/app-version";
import { settingsStyles } from "@/styles/settings";
import { THINKING_TONE_NATIVE_PCM_BASE64 } from "@/utils/thinking-tone.native-pcm";
import { useVoiceAudioEngineOptional } from "@/contexts/voice-context";
const delay = (ms: number) =>
new Promise<void>((resolve) => {
@@ -484,6 +488,7 @@ function DesktopAppUpdateRow() {
export default function SettingsScreen() {
const { theme } = useUnistyles();
const insets = useSafeAreaInsets();
const voiceAudioEngine = useVoiceAudioEngineOptional();
const params = useLocalSearchParams<{ editHost?: string; serverId?: string }>();
const routeServerId = typeof params.serverId === "string" ? params.serverId.trim() : "";
const { settings, isLoading: settingsLoading, updateSettings } = useAppSettings();
@@ -500,6 +505,8 @@ export default function SettingsScreen() {
const [isRemovingHost, setIsRemovingHost] = useState(false);
const [editingDaemon, setEditingDaemon] = useState<HostProfile | null>(null);
const [isSavingEdit, setIsSavingEdit] = useState(false);
const [isPlaybackTestRunning, setIsPlaybackTestRunning] = useState(false);
const [playbackTestResult, setPlaybackTestResult] = useState<string | null>(null);
const isLoading = settingsLoading;
const isMountedRef = useRef(true);
const lastHandledEditHostRef = useRef<string | null>(null);
@@ -659,6 +666,35 @@ export default function SettingsScreen() {
const restartConfirmationMessage =
"This will restart the daemon. The app will reconnect automatically.";
const handlePlaybackTest = useCallback(async () => {
if (!voiceAudioEngine || isPlaybackTestRunning) {
return;
}
setIsPlaybackTestRunning(true);
setPlaybackTestResult(null);
try {
const bytes = Buffer.from(THINKING_TONE_NATIVE_PCM_BASE64, "base64");
await voiceAudioEngine.initialize();
voiceAudioEngine.stop();
await voiceAudioEngine.play({
type: "audio/pcm;rate=16000;bits=16",
size: bytes.byteLength,
async arrayBuffer() {
return Uint8Array.from(bytes).buffer;
},
});
setPlaybackTestResult("Playback command completed.");
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error("[Settings] Playback test failed", error);
setPlaybackTestResult(`Playback failed: ${message}`);
} finally {
setIsPlaybackTestRunning(false);
}
}, [isPlaybackTestRunning, voiceAudioEngine]);
if (isLoading) {
return (
<View style={styles.loadingContainer}>
@@ -869,6 +905,31 @@ export default function SettingsScreen() {
{isDesktop ? <DesktopPermissionsSection /> : null}
{isDesktop ? <LocalDaemonSection appVersion={appVersion} /> : null}
<View style={settingsStyles.section}>
<Text style={settingsStyles.sectionTitle}>Diagnostics</Text>
<View style={[settingsStyles.card, styles.audioCard]}>
<View style={styles.audioRow}>
<View style={styles.audioRowContent}>
<Text style={styles.audioRowTitle}>Test audio playback</Text>
<Text style={styles.aboutHintText}>
Plays a built-in PCM sample through the current app audio engine.
</Text>
{playbackTestResult ? (
<Text style={styles.aboutHintText}>{playbackTestResult}</Text>
) : null}
</View>
<Button
variant="secondary"
size="sm"
onPress={() => void handlePlaybackTest()}
disabled={!voiceAudioEngine || isPlaybackTestRunning}
>
{isPlaybackTestRunning ? "Playing..." : "Play test"}
</Button>
</View>
</View>
</View>
{/* About */}
<View style={settingsStyles.section}>
<Text style={settingsStyles.sectionTitle}>About</Text>
@@ -1393,18 +1454,22 @@ function DaemonCard({
</View>
) : null}
<Button
variant="ghost"
size="sm"
<Pressable
style={styles.hostSettingsButton}
textStyle={{ fontSize: 0, lineHeight: 0 }}
leftIcon={<Settings size={theme.iconSize.md} color={theme.colors.foregroundMuted} />}
onPress={() => onOpenSettings(daemon)}
testID={`daemon-card-settings-${daemon.serverId}`}
accessibilityLabel={`Open settings for ${daemon.label}`}
accessibilityRole="button"
>
{" "}
</Button>
{({ hovered = false, pressed = false }) => (
<Settings
size={theme.iconSize.md}
color={
hovered || pressed ? theme.colors.foreground : theme.colors.foregroundMuted
}
/>
)}
</Pressable>
</View>
</View>
{connectionError ? <Text style={styles.hostError}>{connectionError}</Text> : null}

View File

@@ -23,7 +23,4 @@ export interface AudioEngine {
stop(): void;
clearQueue(): void;
isPlaying(): boolean;
playLooping(pcm: Uint8Array, gapMs: number): void;
stopLooping(): void;
}

View File

@@ -1,35 +1,31 @@
import { Audio } from "expo-av";
import * as FileSystem from "expo-file-system";
import {
THINKING_TONE_REPEAT_GAP_MS,
} from "@/utils/thinking-tone";
import {
THINKING_TONE_NATIVE_PCM_DURATION_MS,
} from "@/utils/thinking-tone.native-pcm";
import type {
AudioEngine,
AudioEngineCallbacks,
AudioPlaybackSource,
} from "@/voice/audio-engine-types";
// Use expo-av for playback instead of expo-two-way-audio (temporary test)
const USE_EXPO_AV_PLAYBACK = true;
interface QueuedAudio {
audio: AudioPlaybackSource;
resolve: (duration: number) => void;
reject: (error: Error) => void;
}
interface CuePcm {
pcm16k: Uint8Array;
durationMs: number;
}
interface AudioEngineTraceOptions {
traceLabel?: string;
}
let nextAudioEngineInstanceId = 1;
interface BridgeStats {
windowStartedAtMs: number;
captureEvents: number;
captureBytes: number;
playbackEvents: number;
playbackInputBytes: number;
playbackResampledBytes: number;
playbackDurationMs: number;
}
function parsePcmSampleRate(mimeType: string): number | null {
const match = /rate=(\d+)/i.exec(mimeType);
if (!match) {
@@ -39,10 +35,92 @@ function parsePcmSampleRate(mimeType: string): number | null {
return Number.isFinite(rate) && rate > 0 ? rate : null;
}
function resamplePcm16(pcm: Uint8Array, fromRate: number, toRate: number): Uint8Array {
if (fromRate === toRate) {
return pcm;
}
const inputSamples = Math.floor(pcm.length / 2);
const outputSamples = Math.floor((inputSamples * toRate) / fromRate);
const out = new Uint8Array(outputSamples * 2);
const ratio = fromRate / toRate;
const readInt16 = (sampleIndex: number): number => {
const i = sampleIndex * 2;
if (i + 1 >= pcm.length) {
return 0;
}
const lo = pcm[i]!;
const hi = pcm[i + 1]!;
let value = (hi << 8) | lo;
if (value & 0x8000) {
value = value - 0x10000;
}
return value;
};
const writeInt16 = (sampleIndex: number, value: number): void => {
const clamped = Math.max(-32768, Math.min(32767, Math.round(value)));
const i = sampleIndex * 2;
out[i] = clamped & 0xff;
out[i + 1] = (clamped >> 8) & 0xff;
};
for (let i = 0; i < outputSamples; i++) {
const srcPos = i * ratio;
const i0 = Math.floor(srcPos);
const frac = srcPos - i0;
const s0 = readInt16(i0);
const s1 = readInt16(Math.min(inputSamples - 1, i0 + 1));
writeInt16(i, s0 + (s1 - s0) * frac);
}
return out;
}
export function createAudioEngine(
callbacks: AudioEngineCallbacks,
_options?: AudioEngineTraceOptions
): AudioEngine {
const native = require("@getpaseo/expo-two-way-audio");
const instanceId = nextAudioEngineInstanceId++;
const bridgeStats: BridgeStats = {
windowStartedAtMs: Date.now(),
captureEvents: 0,
captureBytes: 0,
playbackEvents: 0,
playbackInputBytes: 0,
playbackResampledBytes: 0,
playbackDurationMs: 0,
};
const toHexPreview = (bytes: Uint8Array, count = 12): string =>
Array.from(bytes.slice(0, count))
.map((value) => value.toString(16).padStart(2, "0"))
.join(" ");
const maybeFlushBridgeStats = (reason: string): void => {
const now = Date.now();
const elapsedMs = now - bridgeStats.windowStartedAtMs;
if (elapsedMs < 1000) {
return;
}
console.log(
`[AudioEngine.native#${instanceId}][bridge] ${reason} ` +
`capture=${bridgeStats.captureEvents}ev/${bridgeStats.captureBytes}B ` +
`play=${bridgeStats.playbackEvents}ev/${bridgeStats.playbackInputBytes}B->${bridgeStats.playbackResampledBytes}B ` +
`playMs=${bridgeStats.playbackDurationMs.toFixed(1)} ` +
`windowMs=${elapsedMs}`
);
bridgeStats.windowStartedAtMs = now;
bridgeStats.captureEvents = 0;
bridgeStats.captureBytes = 0;
bridgeStats.playbackEvents = 0;
bridgeStats.playbackInputBytes = 0;
bridgeStats.playbackResampledBytes = 0;
bridgeStats.playbackDurationMs = 0;
};
const refs: {
initialized: boolean;
captureActive: boolean;
@@ -55,14 +133,7 @@ export function createAudioEngine(
reject: (error: Error) => void;
settled: boolean;
} | null;
looping: {
active: boolean;
token: number;
timeout: ReturnType<typeof setTimeout> | null;
};
thinkingTone: CuePcm | null;
destroyed: boolean;
mockVolumeInterval: ReturnType<typeof setInterval> | null;
} = {
initialized: false,
captureActive: false,
@@ -71,62 +142,53 @@ export function createAudioEngine(
processingQueue: false,
playbackTimeout: null,
activePlayback: null,
looping: {
active: false,
token: 0,
timeout: null,
},
thinkingTone: null,
destroyed: false,
mockVolumeInterval: null,
};
// Only subscribe to native events if not mocking
let microphoneSubscription: { remove(): void };
let volumeSubscription: { remove(): void };
const microphoneSubscription = native.addExpoTwoWayAudioEventListener(
"onMicrophoneData",
(event: any) => {
if (!refs.captureActive || refs.muted) {
return;
}
const pcm = event.data as Uint8Array;
bridgeStats.captureEvents += 1;
bridgeStats.captureBytes += pcm.byteLength;
maybeFlushBridgeStats("capture");
callbacks.onCaptureData(pcm);
}
);
const volumeSubscription = native.addExpoTwoWayAudioEventListener(
"onInputVolumeLevelData",
(event: any) => {
if (!refs.captureActive) {
return;
}
const level = refs.muted ? 0 : event.data;
callbacks.onVolumeLevel(level);
}
);
if (MOCK_MODE) {
microphoneSubscription = { remove() {} };
volumeSubscription = { remove() {} };
} else {
const native = require("@boudra/expo-two-way-audio");
microphoneSubscription = native.addExpoTwoWayAudioEventListener(
"onMicrophoneData",
(event: any) => {
if (!refs.captureActive || refs.muted) {
return;
}
callbacks.onCaptureData(event.data);
}
);
volumeSubscription = native.addExpoTwoWayAudioEventListener(
"onInputVolumeLevelData",
(event: any) => {
if (!refs.captureActive) {
return;
}
const level = refs.muted ? 0 : event.data;
callbacks.onVolumeLevel(level);
}
);
}
const outputVolumeSubscription = native.addExpoTwoWayAudioEventListener(
"onOutputVolumeLevelData",
(event: any) => {
console.log(`[AudioEngine.native#${instanceId}] outputVolume=${event.data}`);
}
);
async function ensureInitialized(): Promise<void> {
if (refs.initialized) {
return;
}
if (!MOCK_MODE) {
const native = require("@boudra/expo-two-way-audio");
await native.initialize();
const success = await native.initialize();
if (!success) {
throw new Error("expo-two-way-audio: native initialize() returned false");
}
console.log(`[AudioEngine.native#${instanceId}] initialized successfully`);
refs.initialized = true;
}
async function ensureMicrophonePermission(): Promise<void> {
if (MOCK_MODE) {
return;
}
const native = require("@boudra/expo-two-way-audio");
let permission = await native.getMicrophonePermissionsAsync().catch(() => null);
if (!permission?.granted) {
permission = await native.requestMicrophonePermissionsAsync().catch(() => null);
@@ -138,15 +200,6 @@ export function createAudioEngine(
}
}
async function ensureThinkingTone(): Promise<CuePcm> {
if (refs.thinkingTone) {
return refs.thinkingTone;
}
const durationMs = THINKING_TONE_NATIVE_PCM_DURATION_MS;
refs.thinkingTone = { pcm16k: new Uint8Array(0), durationMs };
return refs.thinkingTone;
}
function clearPlaybackTimeout(): void {
if (refs.playbackTimeout) {
clearTimeout(refs.playbackTimeout);
@@ -164,15 +217,24 @@ export function createAudioEngine(
const arrayBuffer = await audio.arrayBuffer();
const pcm = new Uint8Array(arrayBuffer);
const inputRate = parsePcmSampleRate(audio.type || "") ?? 24000;
const durationSec = pcm.length / 2 / inputRate;
console.log(`[AudioEngine.native] playPCMData: MOCK_MODE=${MOCK_MODE} inputRate=${inputRate} inputBytes=${pcm.length} durationSec=${durationSec.toFixed(3)}`);
// Native AudioEngine expects 16kHz PCM16
const pcm16k = resamplePcm16(pcm, inputRate, 16000);
const durationSec = pcm16k.length / 2 / 16000;
bridgeStats.playbackEvents += 1;
bridgeStats.playbackInputBytes += pcm.length;
bridgeStats.playbackResampledBytes += pcm16k.length;
bridgeStats.playbackDurationMs += durationSec * 1000;
if (!MOCK_MODE) {
const native = require("@boudra/expo-two-way-audio");
native.resumePlayback();
native.playPCMData(pcm);
}
console.log(
`[AudioEngine.native#${instanceId}] playPCMData: inputRate=${inputRate} inputBytes=${pcm.length} ` +
`resampled=${pcm16k.length} durationSec=${durationSec.toFixed(3)} ` +
`pcmHead=${toHexPreview(pcm)} resampledHead=${toHexPreview(pcm16k)}`
);
maybeFlushBridgeStats("play");
native.resumePlayback();
native.playPCMData(pcm16k);
clearPlaybackTimeout();
refs.playbackTimeout = setTimeout(() => {
@@ -215,27 +277,9 @@ export function createAudioEngine(
refs.processingQueue = false;
}
function nativeStopPlayback(): void {
if (!MOCK_MODE) {
const native = require("@boudra/expo-two-way-audio");
native.stopPlayback();
}
}
function stopLooping(): void {
refs.looping.active = false;
refs.looping.token += 1;
if (refs.looping.timeout) {
clearTimeout(refs.looping.timeout);
refs.looping.timeout = null;
}
nativeStopPlayback();
}
return {
async initialize() {
await ensureInitialized();
await ensureThinkingTone();
},
async destroy() {
@@ -243,30 +287,22 @@ export function createAudioEngine(
return;
}
refs.destroyed = true;
stopLooping();
this.stop();
this.clearQueue();
if (refs.captureActive) {
if (!MOCK_MODE) {
const native = require("@boudra/expo-two-way-audio");
native.toggleRecording(false);
}
if (refs.mockVolumeInterval) {
clearInterval(refs.mockVolumeInterval);
refs.mockVolumeInterval = null;
}
native.toggleRecording(false);
refs.captureActive = false;
}
clearPlaybackTimeout();
refs.muted = false;
callbacks.onVolumeLevel(0);
if (refs.initialized && !MOCK_MODE) {
const native = require("@boudra/expo-two-way-audio");
if (refs.initialized) {
native.tearDown();
refs.initialized = false;
}
microphoneSubscription.remove();
volumeSubscription.remove();
outputVolumeSubscription.remove();
},
async startCapture() {
@@ -277,22 +313,7 @@ export function createAudioEngine(
try {
await ensureMicrophonePermission();
await ensureInitialized();
if (MOCK_MODE) {
// Emit fake volume levels and empty PCM data periodically
refs.mockVolumeInterval = setInterval(() => {
if (!refs.captureActive) {
return;
}
const fakeVolume = refs.muted ? 0 : 0.15 + Math.random() * 0.5;
callbacks.onVolumeLevel(fakeVolume);
// Send empty PCM chunk to keep the pipeline alive
callbacks.onCaptureData(new Uint8Array(3200));
}, 100);
} else {
const native = require("@boudra/expo-two-way-audio");
native.toggleRecording(true);
}
native.toggleRecording(true);
refs.captureActive = true;
} catch (error) {
const wrapped = error instanceof Error ? error : new Error(String(error));
@@ -303,15 +324,7 @@ export function createAudioEngine(
async stopCapture() {
if (refs.captureActive) {
if (MOCK_MODE) {
if (refs.mockVolumeInterval) {
clearInterval(refs.mockVolumeInterval);
refs.mockVolumeInterval = null;
}
} else {
const native = require("@boudra/expo-two-way-audio");
native.toggleRecording(false);
}
native.toggleRecording(false);
}
refs.captureActive = false;
refs.muted = false;
@@ -340,7 +353,7 @@ export function createAudioEngine(
},
stop() {
nativeStopPlayback();
native.stopPlayback();
clearPlaybackTimeout();
const active = refs.activePlayback;
refs.activePlayback = null;
@@ -360,45 +373,5 @@ export function createAudioEngine(
isPlaying() {
return refs.activePlayback !== null;
},
playLooping(audio, gapMs) {
if (refs.looping.active) {
return;
}
refs.looping.active = true;
const token = refs.looping.token + 1;
refs.looping.token = token;
void (async () => {
try {
await ensureInitialized();
const cue = await ensureThinkingTone();
const loop = () => {
if (!refs.looping.active || refs.looping.token !== token) {
return;
}
if (!MOCK_MODE) {
const native = require("@boudra/expo-two-way-audio");
native.resumePlayback();
native.playPCMData(cue.pcm16k);
}
refs.looping.timeout = setTimeout(
loop,
cue.durationMs + (gapMs || THINKING_TONE_REPEAT_GAP_MS)
);
};
loop();
} catch (error) {
callbacks.onError?.(
error instanceof Error ? error : new Error(String(error))
);
}
})();
},
stopLooping,
};
}

View File

@@ -1,8 +1,4 @@
import { getTauri } from "@/utils/tauri";
import {
loadThinkingToneArrayBuffer,
THINKING_TONE_REPEAT_GAP_MS,
} from "@/utils/thinking-tone";
import type {
AudioEngine,
AudioEngineCallbacks,
@@ -117,16 +113,6 @@ export function createAudioEngine(
reject: (error: Error) => void;
settled: boolean;
} | null;
looping: {
token: number;
source: AudioBufferSourceNode | null;
gain: GainNode | null;
timeout: ReturnType<typeof setTimeout> | null;
};
thinkingTone: {
bytes: Uint8Array | null;
buffer: AudioBuffer | null;
};
} = {
playbackContext: null,
captureContext: null,
@@ -139,16 +125,6 @@ export function createAudioEngine(
queue: [],
processingQueue: false,
activePlayback: null,
looping: {
token: 0,
source: null,
gain: null,
timeout: null,
},
thinkingTone: {
bytes: null,
buffer: null,
},
};
async function ensurePlaybackContext(): Promise<AudioContext> {
@@ -193,48 +169,6 @@ export function createAudioEngine(
return context;
}
async function ensureThinkingToneBytes(): Promise<Uint8Array> {
if (refs.thinkingTone.bytes) {
return refs.thinkingTone.bytes;
}
const wav = await loadThinkingToneArrayBuffer();
refs.thinkingTone.bytes = new Uint8Array(wav);
return refs.thinkingTone.bytes;
}
async function ensureThinkingToneBuffer(): Promise<AudioBuffer> {
if (refs.thinkingTone.buffer) {
return refs.thinkingTone.buffer;
}
const context = await ensurePlaybackContext();
const wav = await loadThinkingToneArrayBuffer();
const audioBuffer = await decodeAudioData(context, wav);
refs.thinkingTone.buffer = audioBuffer;
return audioBuffer;
}
function stopLooping(): void {
refs.looping.token += 1;
if (refs.looping.timeout) {
clearTimeout(refs.looping.timeout);
refs.looping.timeout = null;
}
if (refs.looping.source) {
try {
refs.looping.source.onended = null;
refs.looping.source.stop();
} catch {
// Ignore best-effort stop errors.
}
refs.looping.source.disconnect();
refs.looping.source = null;
}
if (refs.looping.gain) {
refs.looping.gain.disconnect();
refs.looping.gain = null;
}
}
async function playAudio(audio: AudioPlaybackSource): Promise<number> {
const context = await ensurePlaybackContext();
const arrayBuffer = await audio.arrayBuffer();
@@ -335,18 +269,15 @@ export function createAudioEngine(
return {
async initialize() {
await ensurePlaybackContext();
await Promise.all([ensureThinkingToneBytes(), ensureThinkingToneBuffer()]);
},
async destroy() {
stopLooping();
this.stop();
this.clearQueue();
await stopCapture();
const playbackContext = refs.playbackContext;
refs.playbackContext = null;
refs.thinkingTone.buffer = null;
if (playbackContext && playbackContext.state !== "closed") {
await playbackContext.close().catch(() => undefined);
}
@@ -486,63 +417,5 @@ export function createAudioEngine(
isPlaying() {
return refs.activePlayback !== null;
},
playLooping(audio, gapMs) {
if (refs.looping.source || refs.looping.timeout) {
return;
}
const token = refs.looping.token + 1;
refs.looping.token = token;
void (async () => {
try {
const context = await ensurePlaybackContext();
const audioBuffer =
audio.byteLength > 0
? pcm16LeToAudioBuffer(context, audio, 16000)
: await ensureThinkingToneBuffer();
if (refs.looping.token !== token) {
return;
}
const gain = context.createGain();
gain.gain.value = 1;
gain.connect(context.destination);
refs.looping.gain = gain;
const playOnce = () => {
if (refs.looping.token !== token) {
return;
}
const source = context.createBufferSource();
source.buffer = audioBuffer;
source.connect(gain);
refs.looping.source = source;
source.onended = () => {
if (refs.looping.source === source) {
refs.looping.source = null;
}
if (refs.looping.token !== token) {
return;
}
refs.looping.timeout = setTimeout(
playOnce,
gapMs || THINKING_TONE_REPEAT_GAP_MS
);
};
source.start();
};
playOnce();
} catch (error) {
callbacks.onError?.(
error instanceof Error ? error : new Error(String(error))
);
}
})();
},
stopLooping,
};
}

View File

@@ -20,8 +20,6 @@ function createAudioEngineMock(): AudioEngine {
stop: vi.fn(),
clearQueue: vi.fn(),
isPlaying: vi.fn().mockReturnValue(false),
playLooping: vi.fn(),
stopLooping: vi.fn(),
};
}
@@ -137,7 +135,6 @@ describe("voice runtime", () => {
runtime.onAssistantAudioStarted("server-1");
expect(runtime.getSnapshot().phase).toBe("playing");
expect(engine.stopLooping).toHaveBeenCalled();
expect(adapter.setAssistantAudioPlaying).toHaveBeenCalledWith(true);
});
@@ -256,7 +253,7 @@ describe("voice runtime", () => {
runtime.onAssistantAudioFinished("server-1");
expect(runtime.getSnapshot().phase).toBe("waiting");
expect(engine.playLooping).toHaveBeenCalled();
expect(engine.play).toHaveBeenCalled();
});
it("starts the thinking tone when an agent turn begins before playback", async () => {
@@ -268,7 +265,7 @@ describe("voice runtime", () => {
runtime.onTurnEvent("server-1", "agent-1", "turn_started");
expect(runtime.getSnapshot().phase).toBe("waiting");
expect(engine.playLooping).toHaveBeenCalled();
expect(engine.play).toHaveBeenCalled();
});
it("returns to listening after assistant playback once the turn is complete", async () => {
@@ -283,8 +280,7 @@ describe("voice runtime", () => {
runtime.onAssistantAudioFinished("server-1");
expect(runtime.getSnapshot().phase).toBe("listening");
expect(engine.playLooping).toHaveBeenCalledTimes(1);
expect(engine.stopLooping).toHaveBeenCalled();
expect(engine.play).toHaveBeenCalled();
});
it("keeps local volume alone non-authoritative for playback interruption", async () => {

View File

@@ -7,6 +7,10 @@ import { resolveVoiceUnavailableMessage } from "@/utils/server-info-capabilities
import type { DaemonServerInfo } from "@/stores/session-store";
import type { AudioEngine } from "@/voice/audio-engine-types";
import { REALTIME_VOICE_VAD_CONFIG } from "@/voice/realtime-voice-config";
import {
THINKING_TONE_NATIVE_PCM_BASE64,
THINKING_TONE_NATIVE_PCM_DURATION_MS,
} from "@/utils/thinking-tone.native-pcm";
const PCM_MIME_TYPE = "audio/pcm;rate=16000;bits=16";
const KEEP_AWAKE_TAG = "paseo:voice";
@@ -114,6 +118,26 @@ interface RuntimePlaybackState {
generation: number;
}
interface CueState {
active: boolean;
token: number;
timeout: ReturnType<typeof setTimeout> | null;
playing: boolean;
}
interface RealtimeBridgeStats {
windowStartedAtMs: number;
captureEvents: number;
captureBytes: number;
uplinkEvents: number;
uplinkRawBytes: number;
uplinkBase64Chars: number;
outputEvents: number;
outputBytes: number;
outputGroups: number;
jsLagMaxMs: number;
}
const INITIAL_SNAPSHOT: VoiceRuntimeSnapshot = {
phase: "disabled",
isVoiceMode: false,
@@ -130,6 +154,8 @@ const INITIAL_TELEMETRY: VoiceRuntimeTelemetrySnapshot = {
segmentDuration: 0,
};
let nextVoiceRuntimeInstanceId = 1;
function snapshotsEqual(
left: VoiceRuntimeSnapshot,
right: VoiceRuntimeSnapshot
@@ -180,6 +206,7 @@ export interface VoiceRuntime {
}
export function createVoiceRuntime(deps: VoiceRuntimeDeps): VoiceRuntime {
const instanceId = nextVoiceRuntimeInstanceId++;
const listeners = new Set<() => void>();
const telemetryListeners = new Set<() => void>();
const sessions = new Map<string, RuntimeSessionState>();
@@ -201,6 +228,69 @@ export function createVoiceRuntime(deps: VoiceRuntimeDeps): VoiceRuntime {
processing: false,
generation: 0,
};
const bridgeStats: RealtimeBridgeStats = {
windowStartedAtMs: Date.now(),
captureEvents: 0,
captureBytes: 0,
uplinkEvents: 0,
uplinkRawBytes: 0,
uplinkBase64Chars: 0,
outputEvents: 0,
outputBytes: 0,
outputGroups: 0,
jsLagMaxMs: 0,
};
const cue: CueState = {
active: false,
token: 0,
timeout: null,
playing: false,
};
const cuePcm16 = Uint8Array.from(Buffer.from(THINKING_TONE_NATIVE_PCM_BASE64, "base64"));
const cueSource = {
size: cuePcm16.byteLength,
type: "audio/pcm;rate=16000;bits=16",
async arrayBuffer() {
return cuePcm16.buffer.slice(
cuePcm16.byteOffset,
cuePcm16.byteOffset + cuePcm16.byteLength
);
},
};
let lagProbeLastMs = Date.now();
const lagProbe = setInterval(() => {
const now = Date.now();
const lagMs = Math.max(0, now - lagProbeLastMs - 100);
lagProbeLastMs = now;
if (lagMs > bridgeStats.jsLagMaxMs) {
bridgeStats.jsLagMaxMs = lagMs;
}
}, 100);
function flushBridgeStats(reason: string): void {
const now = Date.now();
const elapsedMs = now - bridgeStats.windowStartedAtMs;
if (elapsedMs < 1000) {
return;
}
console.log(
`[VoiceRuntime#${instanceId}][bridge] ${reason} ` +
`capture=${bridgeStats.captureEvents}ev/${bridgeStats.captureBytes}B ` +
`uplink=${bridgeStats.uplinkEvents}ev/${bridgeStats.uplinkRawBytes}B/${bridgeStats.uplinkBase64Chars}c ` +
`output=${bridgeStats.outputEvents}ev/${bridgeStats.outputBytes}B groups=${bridgeStats.outputGroups} ` +
`jsLagMaxMs=${bridgeStats.jsLagMaxMs} windowMs=${elapsedMs}`
);
bridgeStats.windowStartedAtMs = now;
bridgeStats.captureEvents = 0;
bridgeStats.captureBytes = 0;
bridgeStats.uplinkEvents = 0;
bridgeStats.uplinkRawBytes = 0;
bridgeStats.uplinkBase64Chars = 0;
bridgeStats.outputEvents = 0;
bridgeStats.outputBytes = 0;
bridgeStats.outputGroups = 0;
bridgeStats.jsLagMaxMs = 0;
}
function emit(): void {
for (const listener of listeners) {
@@ -441,7 +531,17 @@ export function createVoiceRuntime(deps: VoiceRuntimeDeps): VoiceRuntime {
}
function stopCue(): void {
deps.engine.stopLooping();
const hadActive = cue.active || cue.timeout !== null || cue.playing;
cue.active = false;
cue.token += 1;
if (cue.timeout) {
clearTimeout(cue.timeout);
cue.timeout = null;
}
if (hadActive) {
deps.engine.stop();
deps.engine.clearQueue();
}
}
function resetCaptureTelemetry(): void {
@@ -455,7 +555,36 @@ export function createVoiceRuntime(deps: VoiceRuntimeDeps): VoiceRuntime {
stopCue();
return;
}
deps.engine.playLooping(new Uint8Array(0), THINKING_TONE_REPEAT_GAP_MS);
if (cue.active) {
return;
}
cue.active = true;
cue.token += 1;
const token = cue.token;
const playNext = () => {
if (!cue.active || cue.token !== token) {
return;
}
cue.playing = true;
void deps.engine
.play(cueSource)
.catch((error) => {
if (cue.token !== token) {
return;
}
console.warn(`[VoiceRuntime#${instanceId}] Cue playback failed:`, error);
})
.finally(() => {
cue.playing = false;
if (!cue.active || cue.token !== token) {
return;
}
cue.timeout = setTimeout(playNext, THINKING_TONE_NATIVE_PCM_DURATION_MS + THINKING_TONE_REPEAT_GAP_MS);
});
};
playNext();
}
const uploader: ContinuousVoiceUploader = {
@@ -471,10 +600,18 @@ export function createVoiceRuntime(deps: VoiceRuntimeDeps): VoiceRuntime {
return;
}
const base64 = Buffer.from(chunk).toString("base64");
bridgeStats.captureEvents += 1;
bridgeStats.captureBytes += chunk.byteLength;
bridgeStats.uplinkEvents += 1;
bridgeStats.uplinkRawBytes += chunk.byteLength;
bridgeStats.uplinkBase64Chars += base64.length;
flushBridgeStats("uplink");
void activeSession.adapter
.sendVoiceAudioChunk(Buffer.from(chunk).toString("base64"), PCM_MIME_TYPE)
.sendVoiceAudioChunk(base64, PCM_MIME_TYPE)
.catch((error) => {
console.error("[VoiceRuntime] Failed to send audio chunk:", error);
console.error(`[VoiceRuntime#${instanceId}] Failed to send audio chunk:`, error);
});
},
};
@@ -651,13 +788,22 @@ export function createVoiceRuntime(deps: VoiceRuntimeDeps): VoiceRuntime {
!state.snapshot.isVoiceMode ||
!payload.isVoiceMode
) {
console.log(`[VoiceRuntime] audio_output DROPPED: activeServer=${state.snapshot.activeServerId} serverId=${serverId} isVoiceMode=${state.snapshot.isVoiceMode} payloadVoice=${payload.isVoiceMode}`);
console.log(`[VoiceRuntime#${instanceId}] audio_output DROPPED: activeServer=${state.snapshot.activeServerId} serverId=${serverId} isVoiceMode=${state.snapshot.isVoiceMode} payloadVoice=${payload.isVoiceMode}`);
return;
}
const groupId = payload.groupId ?? payload.id;
const chunkIndex = payload.chunkIndex ?? 0;
console.log(`[VoiceRuntime] audio_output groupId=${groupId} chunk=${chunkIndex} isLast=${payload.isLastChunk} size=${payload.audio.length} format=${payload.format}`);
const decoded = decodeAudioChunk(payload.audio);
bridgeStats.outputEvents += 1;
bridgeStats.outputBytes += decoded.byteLength;
bridgeStats.outputGroups += playback.groups.has(groupId) ? 0 : 1;
console.log(
`[VoiceRuntime#${instanceId}] audio_output groupId=${groupId} chunk=${chunkIndex} isLast=${payload.isLastChunk} ` +
`base64Chars=${payload.audio.length} decodedBytes=${decoded.byteLength} format=${payload.format} ` +
`head=${Array.from(decoded.slice(0, 12)).map((value) => value.toString(16).padStart(2, "0")).join(" ")}`
);
flushBridgeStats("audio_output");
let group = playback.groups.get(groupId);
if (!group) {
@@ -683,7 +829,7 @@ export function createVoiceRuntime(deps: VoiceRuntimeDeps): VoiceRuntime {
group.chunks.set(chunkIndex, {
id: payload.id,
chunkIndex,
source: toPlaybackSource(decodeAudioChunk(payload.audio), payload.format),
source: toPlaybackSource(decoded, payload.format),
});
if (payload.isLastChunk) {
group.finalChunkIndex = chunkIndex;
@@ -796,6 +942,7 @@ export function createVoiceRuntime(deps: VoiceRuntimeDeps): VoiceRuntime {
async destroy() {
await this.stopVoice().catch(() => undefined);
clearInterval(lagProbe);
await deps.engine.destroy();
listeners.clear();
telemetryListeners.clear();

View File

@@ -0,0 +1,5 @@
module.exports = {
root: true,
extends: ["universe/native", "universe/web"],
ignorePatterns: ["build"],
};

View File

@@ -0,0 +1,29 @@
name: Publish to NPM
on:
push:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: ⬇️ Checkout
uses: actions/checkout@v4
- name: ⎔ Get Node Version
id: node_version
run: echo "NVMRC=$(cat .nvmrc)" >> $GITHUB_OUTPUT
- name: ⎔ Setup Node
uses: actions/setup-node@v3
with:
node-version: "${{ steps.node_version.outputs.NVMRC }}"
cache: npm
- name: 📦 Install Dependencies
run: npm ci
- name: ⬆️ Publish
run: npm config set '//registry.npmjs.org/:_authToken' "${NODE_AUTH_TOKEN}" && npm publish --access public
env:
NODE_AUTH_TOKEN: ${{secrets.NPM_AUTH_TOKEN}}

View File

@@ -0,0 +1,36 @@
name: 🧪 Test
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: ⬇️ Checkout
uses: actions/checkout@v3
- name: ⎔ Get Node Version
id: node_version
run: echo "NVMRC=$(cat .nvmrc)" >> $GITHUB_OUTPUT
- name: ⎔ Setup Node
uses: actions/setup-node@v3
with:
node-version: "${{ steps.node_version.outputs.NVMRC }}"
cache: npm
- name: 📦 Install Dependencies
run: npm ci
- name: 🧪 Lint
run: npm run lint
- name: 🧪 Format
run: npm run format
- name: 🧪 Typecheck
run: npm run typecheck

56
packages/expo-two-way-audio/.gitignore vendored Normal file
View File

@@ -0,0 +1,56 @@
# OSX
#
.DS_Store
# VSCode
.vscode/
jsconfig.json
# Xcode
#
build/
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata
*.xccheckout
*.moved-aside
DerivedData
*.hmap
*.ipa
*.xcuserstate
project.xcworkspace
# Android/IJ
#
.classpath
.cxx
.gradle
.idea
.project
.settings
local.properties
android.iml
android/app/libs
android/keystores/debug.keystore
# Ruby
examples/*/vendor/
# node.js
#
node_modules/
npm-debug.log
yarn-debug.log
yarn-error.log
# Expo
.expo/*
examples/*/ios
examples/*/android

View File

@@ -0,0 +1,14 @@
# Exclude all top-level hidden directories by convention
/.*/
# Exclude tarballs generated by `npm pack`
/*.tgz
__mocks__
__tests__
/babel.config.js
/android/src/androidTest/
/android/src/test/
/android/build/
/examples/

View File

@@ -0,0 +1 @@
20.17

View File

@@ -0,0 +1,128 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
devx@speechmatics.com.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.

View File

@@ -0,0 +1,54 @@
# Contributing
Welcome to the Speechmatics Expo Two Way Audio Module! We're open to contributions from anyone. We hope you can find everything you need in here to get started contributing to this repo.
## Table of Contents
- [Useful Links](#useful-links)
- [How to Submit Changes](#how-to-submit-changes)
- [How to Report a Bug](#how-to-report-a-bug)
- [How to Request a Feature](#how-to-request-a-feature)
- [Style Guide](#style-guide)
## Useful Links
- [Speechmatics Website](https://www.speechmatics.com/)
- [Portal (for generating API keys)](https://portal.speechmatics.com/manage-access/)
- [Docs](https://docs.speechmatics.com/)
### Installing dependencies
After cloning this repo, run
```sh
npm i
```
## How to Submit Changes
We try not to be too prescreptive about how people work, but we also believe in helping make things easier by following a couple of basic steps. If you follow these recommendations, it should make the review process simpler and quicker:
1. If your change is large, consider reaching out to us beforehand and discussing the issue. This could save you a lot of time if there are good reasons why we haven't done something.
2. Fork the repo and work on a branch on your fork. Try to give your branches short, descriptive names in the format {type}/{description} e.g. bugfix/missing-try-catch.
3. Make sure your changes are tested - ideally both manually and in the unit tests.
4. When opening a PR back into our repo, provide some simple descriptive comments that list the changes being made in the PR.
5. Give your PR a short, descriptive title.
## How to Report a Bug
If you are experiencing a bug, you can report it via the [issues](https://github.com/speechmatics/expo-two-way-audio/issues) page. The more details you give us, the better we can understand and fix your problem!
## How to Request a Feature
If you want a feature, you can open a discussion via the [issues](https://github.com/speechmatics/expo-two-way-audio/issues) page. Try to tag your issue with the most appropriate label available so that we can track it more easily.
## Style Guide
We use Biome for linting and formatting: https://www.npmjs.com/package/@biomejs/biome
You can run linting and formatting using the scripts:
```
npm run lint
npm run format
```

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023, Cantab Research Ltd.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,95 @@
# Speechmatics Two Way Audio
Expo module for capturing and playing pcm audio data in react-native apps (iOS and Android).
The aim of the module is to facilitate creating real-time conversational apps. The following features are provided:
- Request audio recording permissions
- Get clean (applying Acoustic Echo Cancelling) microphone samples in PCM format (1 channel 16 bit at 16kHz)
- Play audio samples in PCM format (1 channel 16 bit at 16kHz). Playback happens through main speaker unless external audio sources are connected.
- Provide volume level both for the input and output samples. Float between 0 and 1.
- [iOS only] Get microphone mode and prompt user to select a microphone mode.
Check out our [examples/](./examples) to see the module in action.
## Installation
```
npm i @speechmatics/expo-two-way-audio
```
## Usage
Please check out our [examples/](./examples) to get full sample code.
1. Request permissions for recording audio
```JSX
import {useMicrophonePermissions} from "@speechmatics/expo-two-way-audio";
const [micPermission, requestMicPermission] = useMicrophonePermissions();
console.log(micPermission);
```
1. Initialize the module before calling any audio functionality.
```JSX
useEffect(() => {
const initializeAudio = async () => {
await initialize();
};
initializeAudio();
}, []);
```
1. Play audio
> [!NOTE]
> The sample below uses the `buffer` module:
> `npm install buffer`
```JSX
import { Buffer } from "buffer";
// As an example, let's play pcm data hardcoded in a variable.
// The examples/basic-usage does this. Check it out for real base64 data.
const audioChunk = "SOME PCM DATA BASE64 ENCODED HERE"
const buffer = Buffer.from(audioChunk, "base64");
const pcmData = new Uint8Array(buffer);
playPCMData(pcmData);
```
1. Get microphone samples
```JSX
// Set up a function to deal with microphone sample events.
// In this case just print the data in the console.
useExpoTwoWayAudioEventListener(
"onMicrophoneData",
useCallback<MicrophoneDataCallback>((event) => {
console.log(`MIC DATA: ${event.data}`);
}, []),
);
// Unmute the microphone to get microphone data events
toggleRecording(true);
```
## Notes
Some audio features of expo-two-way-audio like Acoustic Echo Cancelling, noise reduction or microphone modes (iOS) don't work on simulator. Run the example on a real device to test these features.
```bash
# iOS
npx expo run:ios --device --configuration Release
# Android
npx expo run:android --device --variant release
```
For Android, the following permissions are needed: `RECORD_AUDIO`, `MODIFY_AUDIO_SETTINGS`. In Expo apps they can bee added in your `app.json` file:
```javascript
expo.android.permissions: ["RECORD_AUDIO", "MODIFY_AUDIO_SETTINGS"]
```

View File

@@ -0,0 +1,43 @@
apply plugin: 'com.android.library'
group = 'expo.modules.twowayaudio'
version = '0.1.0'
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
apply from: expoModulesCorePlugin
applyKotlinExpoModulesCorePlugin()
useCoreDependencies()
useExpoPublishing()
// If you want to use the managed Android SDK versions from expo-modules-core, set this to true.
// The Android SDK versions will be bumped from time to time in SDK releases and may introduce breaking changes in your module code.
// Most of the time, you may like to manage the Android SDK versions yourself.
def useManagedAndroidSdkVersions = false
if (useManagedAndroidSdkVersions) {
useDefaultAndroidSdkVersions()
} else {
buildscript {
// Simple helper that allows the root project to override versions declared by this library.
ext.safeExtGet = { prop, fallback ->
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
}
}
project.android {
compileSdkVersion safeExtGet("compileSdkVersion", 34)
defaultConfig {
minSdkVersion safeExtGet("minSdkVersion", 21)
targetSdkVersion safeExtGet("targetSdkVersion", 34)
}
}
}
android {
namespace "expo.modules.twowayaudio"
defaultConfig {
versionCode 1
versionName "0.1.0"
}
lintOptions {
abortOnError false
}
}

View File

@@ -0,0 +1,2 @@
<manifest>
</manifest>

View File

@@ -0,0 +1,425 @@
import android.annotation.SuppressLint
import android.content.Context
import android.media.AudioAttributes
import android.media.AudioDeviceInfo
import android.media.AudioFocusRequest
import android.media.AudioFormat
import android.media.AudioManager
import android.media.AudioRecord
import android.media.AudioTrack
import android.media.MediaRecorder
import android.media.audiofx.AcousticEchoCanceler
import android.media.audiofx.NoiseSuppressor
import android.os.Build
import android.os.PowerManager
import android.util.Log
import androidx.annotation.RequiresApi
import java.util.LinkedList
import java.util.Queue
import java.util.concurrent.Executors
import kotlin.math.pow
class AudioEngine (context: Context) {
private val SAMPLE_RATE = 16000
private val AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT
private val CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_MONO
private lateinit var audioRecord: AudioRecord
private lateinit var audioManager: AudioManager
private lateinit var audioTrack: AudioTrack
private var audioFocusRequest: AudioFocusRequest? = null
private val audioSampleQueue: Queue<ByteArray> = LinkedList()
private var echoCanceler: AcousticEchoCanceler? = null
private var noiseSuppressor: NoiseSuppressor? = null
private val executorServiceMicrophone = Executors.newFixedThreadPool(1)
private val executorServicePlayback = Executors.newFixedThreadPool(1)
private var speakerDevice: AudioDeviceInfo? = null
private var bridgeWindowStartedAtMs = System.currentTimeMillis()
private var micEvents = 0
private var micBytes = 0L
private var playbackEvents = 0
private var playbackQueuedBytes = 0L
private var playbackWrites = 0
private var playbackWriteBytes = 0L
var isRecording = false
private var isRecordingBeforePause = false
var isPlaying = false
// Callbacks
var onMicDataCallback: ((ByteArray) -> Unit)? = null
var onInputVolumeCallback: ((Float) -> Unit)? = null
var onOutputVolumeCallback: ((Float) -> Unit)? = null
var onAudioInterruptionCallback: ((String) -> Unit)? = null
init {
initializeAudio(context)
}
private fun flushBridgeStats(reason: String) {
val now = System.currentTimeMillis()
val elapsedMs = now - bridgeWindowStartedAtMs
if (elapsedMs < 1000) {
return
}
Log.d(
"AudioEngine",
"[bridge] $reason mic=$micEvents" +
"ev/${micBytes}B playbackQueue=$playbackEvents" +
"ev/${playbackQueuedBytes}B playbackWrites=$playbackWrites" +
"ev/${playbackWriteBytes}B windowMs=$elapsedMs"
)
bridgeWindowStartedAtMs = now
micEvents = 0
micBytes = 0
playbackEvents = 0
playbackQueuedBytes = 0
playbackWrites = 0
playbackWriteBytes = 0
}
@SuppressLint("NewApi")
private fun initializeAudio(context:Context) {
audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
audioManager.mode = AudioManager.MODE_IN_COMMUNICATION
requestAudioFocus()
// Route audio to external device if connected, otherwise route to speaker
updateAudioRouting()
// Listen for changes in audio routing
audioManager.registerAudioDeviceCallback(object:android.media.AudioDeviceCallback(){
override fun onAudioDevicesAdded(addedDevices: Array<out AudioDeviceInfo>?) {
Log.d("AudioEngine", "onAudioDevicesAdded")
super.onAudioDevicesAdded(addedDevices)
updateAudioRouting()
}
override fun onAudioDevicesRemoved(removedDevices: Array<out AudioDeviceInfo>?) {
Log.d("AudioEngine", "onAudioDevicesRemoved")
super.onAudioDevicesRemoved(removedDevices)
updateAudioRouting()
}
}, null)
val bufferSize = AudioTrack.getMinBufferSize(
SAMPLE_RATE,
AudioFormat.CHANNEL_OUT_MONO,
AUDIO_FORMAT
)
audioTrack = AudioTrack(
AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build(),
AudioFormat.Builder()
.setEncoding(AUDIO_FORMAT)
.setSampleRate(SAMPLE_RATE)
.setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
.build(),
bufferSize,
AudioTrack.MODE_STREAM,
audioManager.generateAudioSessionId()
).apply {
play()
}
}
private fun updateAudioRouting() {
val devices = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)
var isExternalDeviceConnected = false
var selectedDevice: AudioDeviceInfo? = null
for (device in devices) {
if (device.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER) {
speakerDevice = device
}
if (device.type == AudioDeviceInfo.TYPE_WIRED_HEADPHONES ||
device.type == AudioDeviceInfo.TYPE_WIRED_HEADSET ||
device.type == AudioDeviceInfo.TYPE_BLUETOOTH_A2DP ||
device.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO) {
isExternalDeviceConnected = true
selectedDevice = device
break
} else if (device.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER) {
selectedDevice = device
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// Use the modern API for Android S and above
try {
selectedDevice?.let {
audioManager.setCommunicationDevice(it)
}
}catch (e:Exception){
Log.e("AudioEngine", "Error setting communication device. Using speaker")
speakerDevice?.let {
audioManager.setCommunicationDevice(it)
}
}
} else {
// Fall back to deprecated method for older Android versions
@Suppress("DEPRECATION")
audioManager.isSpeakerphoneOn = !isExternalDeviceConnected
}
}
@SuppressLint("NewApi")
private fun requestAudioFocus() {
val focusRequest =
AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE)
.setAudioAttributes(
AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build()
)
.setAcceptsDelayedFocusGain(true)
.setOnAudioFocusChangeListener { focusChange ->
when (focusChange) {
AudioManager.AUDIOFOCUS_LOSS -> {
Log.d("AudioEngine", "Audio focus lost")
onAudioInterruptionCallback?.let { it("blocked") }
}
}
}
.build()
audioFocusRequest = focusRequest
val result = audioManager.requestAudioFocus(focusRequest)
if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
throw RuntimeException("Audio focus request failed")
}
}
@RequiresApi(Build.VERSION_CODES.Q)
@SuppressLint("MissingPermission")
private fun startRecording(){
val bufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, AUDIO_FORMAT)
audioRecord = AudioRecord(
MediaRecorder.AudioSource.VOICE_COMMUNICATION,
SAMPLE_RATE,
CHANNEL_CONFIG,
AUDIO_FORMAT,
bufferSize
)
if (audioRecord.state != AudioRecord.STATE_INITIALIZED) {
throw RuntimeException("Audio Record can't initialize!")
}
if (AcousticEchoCanceler.isAvailable()){
echoCanceler = AcousticEchoCanceler.create(audioRecord.audioSessionId)
if (echoCanceler != null) {
echoCanceler?.enabled = true
Log.i("AudioEngine", "Echo Canceler enabled")
}
}
if (NoiseSuppressor.isAvailable()){
noiseSuppressor = NoiseSuppressor.create(audioRecord.audioSessionId)
if (noiseSuppressor != null) {
noiseSuppressor?.enabled = true
Log.i("AudioEngine", "Noise Suppressor enabled")
}
}
audioRecord.startRecording()
isRecording = true
startMicSampleTap()
}
private fun startMicSampleTap(){
executorServiceMicrophone.execute {
val buffer = ByteArray(1024)
try {
while (isRecording) {
val read = audioRecord.read(buffer, 0, buffer.size)
if (read > 0) {
val data = buffer.copyOf(read)
micEvents += 1
micBytes += data.size.toLong()
flushBridgeStats("mic")
val micVolume = calculateRMSLevel(data)
onInputVolumeCallback?.invoke(micVolume)
onMicDataCallback?.invoke(data)
}
}
Log.d("AudioEngine", "Mic sample tap stopped.")
}catch (e: Exception){
Log.e("AudioEngine", "Error reading mic sample data", e)
isRecording = false
tearDown()
throw e
}
}
}
private fun stopRecording() {
if (!isRecording) return
isRecording = false
if (audioRecord.recordingState == AudioRecord.RECORDSTATE_RECORDING) {
audioRecord.stop()
audioRecord.release()
}
onInputVolumeCallback?.invoke(0.0F)
}
@RequiresApi(Build.VERSION_CODES.Q)
fun toggleRecording(value: Boolean): Boolean {
if (value == isRecording) return isRecording
if (value) {
startRecording()
} else {
stopRecording()
}
isRecording = value
return isRecording
}
fun playPCMData(data: ByteArray) {
audioSampleQueue.add(data)
playbackEvents += 1
playbackQueuedBytes += data.size.toLong()
Log.d(
"AudioEngine",
"playPCMData queued bytes=${data.size} queueSize=${audioSampleQueue.size} " +
"head=${data.take(12).joinToString(" ") { byte -> "%02x".format(byte.toInt() and 0xff) }}"
)
flushBridgeStats("queue")
if (!isPlaying) {
playAudioFromSampleQueue()
}
}
private fun playAudioFromSampleQueue() {
executorServicePlayback.execute{
isPlaying = true
try {
while (audioSampleQueue.isNotEmpty()){
val data = audioSampleQueue.poll()
if (data != null){
playSample(data)
val audioVolume = calculateRMSLevel(data)
onOutputVolumeCallback?.invoke(audioVolume)
}else{
break
}
}
}catch (e: Exception){
Log.e("AudioEngine", "Error playing audio", e)
e.printStackTrace()
}finally {
isPlaying = false
onOutputVolumeCallback?.invoke(0.0F)
}
}
}
private fun playSample(data: ByteArray) {
val written = audioTrack.write(data, 0, data.size)
playbackWrites += 1
playbackWriteBytes += written.coerceAtLeast(0).toLong()
Log.d(
"AudioEngine",
"playSample wrote=$written requested=${data.size} playState=${audioTrack.playState} " +
"queueSize=${audioSampleQueue.size}"
)
flushBridgeStats("write")
}
fun bypassVoiceProcessing(bypass: Boolean) {
if (bypass) {
echoCanceler?.enabled = false
noiseSuppressor?.enabled = false
} else {
echoCanceler?.enabled = true
noiseSuppressor?.enabled = true
}
}
@RequiresApi(Build.VERSION_CODES.Q)
fun pauseRecordingAndPlayer() {
isRecordingBeforePause = isRecording
isRecording = toggleRecording(false)
audioTrack.pause()
}
@RequiresApi(Build.VERSION_CODES.Q)
fun resumeRecordingAndPlayer() {
requestAudioFocus()
isRecording = toggleRecording(isRecordingBeforePause)
audioTrack.play()
}
fun stopPlayback() {
audioSampleQueue.clear()
audioTrack.pause()
audioTrack.flush()
isPlaying = false
onOutputVolumeCallback?.invoke(0.0F)
Log.d("AudioEngine", "Playback stopped")
}
fun pausePlayback() {
audioTrack.pause()
Log.d("AudioEngine", "Playback paused")
}
fun resumePlayback() {
audioTrack.play()
Log.d("AudioEngine", "Playback resumed")
}
@SuppressLint("NewApi")
fun tearDown() {
stopRecording()
audioTrack.stop()
audioManager.mode = AudioManager.MODE_NORMAL
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
audioManager.clearCommunicationDevice()
}
audioFocusRequest?.let { request ->
audioManager.abandonAudioFocusRequest(request)
}
executorServiceMicrophone.shutdownNow()
}
private fun calculateRMSLevel(buffer: ByteArray): Float {
val epsilon = 1e-5f // To avoid log(0)
// Convert ByteArray to FloatArray by treating each pair of bytes as a single 16-bit PCM sample
val floatBuffer = FloatArray(buffer.size / 2)
for (i in floatBuffer.indices) {
// Combine two bytes into a 16-bit signed integer
val sample = (buffer[i * 2].toInt() or (buffer[i * 2 + 1].toInt() shl 8)).toShort()
// Normalize sample to -1.0 to 1.0 range for FloatArray
floatBuffer[i] = sample / 32768.0f
}
// Calculate RMS value
val rmsValue = kotlin.math.sqrt(floatBuffer.fold(0f) { acc, sample -> acc + sample * sample } / floatBuffer.size)
// Convert to decibels
val dbValue = 20 * kotlin.math.log10(maxOf(rmsValue, epsilon))
// Normalize decibel value to 0-1 range
// Assuming minimum audible is -80dB and maximum is 0dB
val minDb = -80.0f
val normalizedValue = maxOf(0.0f, minOf(1.0f, (dbValue - minDb) / kotlin.math.abs(minDb)))
// Optional: Apply exponential factor to push smaller values down
val expFactor = 2.0f // Adjust this value to change the curve
val adjustedValue = normalizedValue.pow(expFactor)
return adjustedValue
}
}

View File

@@ -0,0 +1,18 @@
package expo.modules.twowayaudio
import android.app.Activity
import expo.modules.core.interfaces.ReactActivityLifecycleListener
class ExpoTwoWayAudioLifeCycleListener : ReactActivityLifecycleListener {
override fun onPause(activity: Activity?) {
super.onPause(activity)
// At the moment background audio recording and playback is not supported
ExpoTwoWayAudioModule.audioEngine?.pauseRecordingAndPlayer()
}
override fun onResume(activity: Activity?) {
super.onResume(activity)
// At the moment background audio recording and playback is not supported
ExpoTwoWayAudioModule.audioEngine?.resumeRecordingAndPlayer()
}
}

View File

@@ -0,0 +1,138 @@
package expo.modules.twowayaudio
import AudioEngine
import androidx.core.os.bundleOf
import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition
import expo.modules.kotlin.Promise
import expo.modules.interfaces.permissions.Permissions
class ExpoTwoWayAudioModule : Module() {
companion object {
private const val ON_MIC_DATA_EVENT = "onMicrophoneData"
private const val ON_INPUT_VOLUME_LEVEL_EVENT = "onInputVolumeLevelData"
private const val ON_OUTPUT_VOLUME_LEVEL_EVENT = "onOutputVolumeLevelData"
private const val ON_RECORDING_CHANGE_EVENT = "onRecordingChange"
private const val ON_AUDIO_INTERRUPTION_EVENT = "onAudioInterruption"
var audioEngine: AudioEngine? = null
}
override fun definition() = ModuleDefinition {
Name("ExpoTwoWayAudio")
AsyncFunction("initialize") { promise: Promise ->
try {
if (audioEngine != null) {
promise.resolve(true)
return@AsyncFunction
}
audioEngine = appContext.reactContext?.let { AudioEngine(it) }
setupCallbacks()
promise.resolve(true)
} catch (e: Exception) {
promise.resolve(false)
}
}
Function("isRecording") {
audioEngine?.isRecording ?: false
}
Function("toggleRecording") { value: Boolean ->
audioEngine?.let { engine ->
val isRecording = engine.toggleRecording(value)
sendEvent(ON_RECORDING_CHANGE_EVENT, mapOf("data" to isRecording))
isRecording
} ?: false
}
Function("tearDown") {
audioEngine?.tearDown()
audioEngine = null
null
}
Function("restart") {
audioEngine?.resumeRecordingAndPlayer()
sendEvent(ON_RECORDING_CHANGE_EVENT, mapOf(
"data" to (audioEngine?.isRecording ?: false)
))
}
Function("playPCMData") { data: kotlin.ByteArray ->
audioEngine?.playPCMData(data)
}
Function("bypassVoiceProcessing") { bypass: Boolean ->
audioEngine?.bypassVoiceProcessing(bypass)
}
Function("isPlaying") {
audioEngine?.isPlaying ?: false
}
Function("stopPlayback") {
audioEngine?.stopPlayback()
}
Function("pausePlayback") {
audioEngine?.pausePlayback()
}
Function("resumePlayback") {
audioEngine?.resumePlayback()
}
Function("getMicrophoneModeIOS") {
throw UnsupportedOperationException("getMicrophoneModeIOS is only supported on iOS")
}
Function ("setMicrophoneModeIOS") {
throw UnsupportedOperationException("setMicrophoneModeIOS is only supported on iOS")
}
AsyncFunction("getMicrophonePermissionsAsync") { promise: Promise ->
Permissions.getPermissionsWithPermissionsManager(
appContext.permissions,
promise,
android.Manifest.permission.RECORD_AUDIO
)
}
AsyncFunction("requestMicrophonePermissionsAsync") { promise: Promise ->
Permissions.askForPermissionsWithPermissionsManager(
appContext.permissions,
promise,
android.Manifest.permission.RECORD_AUDIO
)
}
// Register events
Events(
ON_MIC_DATA_EVENT,
ON_INPUT_VOLUME_LEVEL_EVENT,
ON_OUTPUT_VOLUME_LEVEL_EVENT,
ON_RECORDING_CHANGE_EVENT,
ON_AUDIO_INTERRUPTION_EVENT
)
}
private fun setupCallbacks() {
audioEngine?.apply {
onMicDataCallback = { data ->
sendEvent(ON_MIC_DATA_EVENT, bundleOf("data" to data))
}
onInputVolumeCallback = { level ->
sendEvent(ON_INPUT_VOLUME_LEVEL_EVENT, bundleOf("data" to level))
}
onOutputVolumeCallback = { level ->
sendEvent(ON_OUTPUT_VOLUME_LEVEL_EVENT, bundleOf("data" to level))
}
onAudioInterruptionCallback = { data ->
sendEvent(ON_AUDIO_INTERRUPTION_EVENT, bundleOf("data" to data))
sendEvent(ON_RECORDING_CHANGE_EVENT, bundleOf(
"data" to (audioEngine?.isRecording ?: false)
))
}
}
}
}

View File

@@ -0,0 +1,11 @@
package expo.modules.twowayaudio
import android.content.Context
import expo.modules.core.interfaces.Package
import expo.modules.core.interfaces.ReactActivityLifecycleListener
class ExpoTwoWayAudioPackage : Package {
override fun createReactActivityLifecycleListeners(activityContext: Context): List<ReactActivityLifecycleListener> {
return listOf(ExpoTwoWayAudioLifeCycleListener())
}
}

View File

@@ -0,0 +1,28 @@
{
"$schema": "https://biomejs.dev/schemas/1.5.3/schema.json",
"organizeImports": {
"enabled": true
},
"files": {
"ignore": [
"./build",
"./node_modules/",
".expo",
"ios",
"android",
"expo-env.d.ts",
"./examples/*/node_modules",
"./examples/*/.expo",
"./examples/*/ios",
"./examples/*/android"
]
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"lineWidth": 100
},
"linter": {
"enabled": false
}
}

View File

@@ -0,0 +1,35 @@
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
# dependencies
node_modules/
# Expo
.expo/
dist/
web-build/
# Native
*.orig.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision
# Metro
.metro-health-check*
# debug
npm-debug.*
yarn-debug.*
yarn-error.*
# macOS
.DS_Store
*.pem
# local env files
.env*.local
# typescript
*.tsbuildinfo

View File

@@ -0,0 +1,676 @@
import { Buffer } from "buffer";
import {
MicrophoneDataCallback,
VolumeLevelCallback,
getMicrophoneModeIOS,
initialize,
playPCMData,
setMicrophoneModeIOS,
toggleRecording,
useExpoTwoWayAudioEventListener,
useIsRecording,
useMicrophonePermissions,
} from "@speechmatics/expo-two-way-audio";
import { Button, StyleSheet, Text, View } from "react-native";
import { Platform } from "react-native";
import { useCallback, useEffect, useState } from "react";
export default function App() {
const [micPermission, requestMicPermission] = useMicrophonePermissions();
console.log(micPermission);
if (!micPermission?.granted) {
return (
<View style={styles.container}>
<Text>Mic permission: {micPermission?.status}</Text>
<Button
title={micPermission?.canAskAgain ? "Request permission" : "Cannot request permissions"}
disabled={!micPermission?.canAskAgain}
onPress={requestMicPermission}
/>
</View>
);
}
return <Testbed />;
}
export function Testbed() {
const [audioInitialized, setAudioInitialized] = useState(false);
const [inputVolumeLevel, setInputVolumeLevel] = useState(0.0);
const [outputVolumeLevel, setOutputVolumeLevel] = useState(0.0);
const micMode = Platform.OS === "ios" ? getMicrophoneModeIOS() : "NO_MIC_MODE_IN_ANDROID";
const playAudio = () => {
for (const dataChunk of audioData) {
for (const audioChunk of dataChunk.audio) {
const buffer = Buffer.from(audioChunk, "base64");
const pcmData = new Uint8Array(buffer);
playPCMData(pcmData);
}
}
};
const isRecording = useIsRecording();
useExpoTwoWayAudioEventListener(
"onMicrophoneData",
useCallback<MicrophoneDataCallback>((event) => {
console.log(`MIC DATA: ${event.data}`);
}, []),
);
useExpoTwoWayAudioEventListener(
"onInputVolumeLevelData",
useCallback<VolumeLevelCallback>((event) => {
setInputVolumeLevel(event.data);
}, []),
);
useExpoTwoWayAudioEventListener(
"onOutputVolumeLevelData",
useCallback<VolumeLevelCallback>((event) => {
setOutputVolumeLevel(event.data);
}, []),
);
useEffect(() => {
initialize().then(() => setAudioInitialized(true));
}, []);
const handleToggleMute = useCallback(() => {
toggleRecording(!isRecording);
}, [isRecording]);
const handleMicToggle = useCallback(() => {
if (Platform.OS === "ios") {
setMicrophoneModeIOS();
} else {
console.log("No mic mode in android");
}
}, []);
return (
<View style={styles.container}>
<View style={styles.buttonContainer}>
<Button title="Play audio" onPress={playAudio} />
<Button title={isRecording ? "Mute" : "Unmute"} onPress={handleToggleMute} />
{isRecording && <Button title="mic mode" onPress={handleMicToggle} />}
</View>
<View style={styles.volumeContainer}>
<Text>{`input:${inputVolumeLevel}`}</Text>
<Text>{`output:${outputVolumeLevel}`}</Text>
<Text>{isRecording ? micMode : "ready"}</Text>
</View>
</View>
);
}
const audioData = [
{
message: "audio",
audio: [
"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",
],
utterance_idx: 0,
text: "Hey there!",
},
{
message: "audio",
audio: [
"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",
],
utterance_idx: 0,
text: "Hey there!",
},
{
message: "audio",
audio: [
"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",
],
utterance_idx: 0,
text: "Hey there!",
},
{
message: "audio",
audio: [
"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",
],
utterance_idx: 0,
text: "Hey there!",
},
{
message: "audio",
audio: [
"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",
],
utterance_idx: 0,
text: "Hey there!",
},
{
message: "audio",
audio: [
"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",
],
utterance_idx: 0,
text: "Hey there!",
},
{
message: "audio",
audio: [
"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",
],
utterance_idx: 0,
text: "Hey there!",
},
{
message: "audio",
audio: [
"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",
],
utterance_idx: 0,
text: "Hey there!",
},
{
message: "audio",
audio: [
"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",
],
utterance_idx: 0,
text: "Hey there!",
},
{
message: "audio",
audio: [
"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",
],
utterance_idx: 0,
text: "Hey there!",
},
{
message: "audio",
audio: [
"wP7V/ur++v4T/xb/Ef8U/xX/Dv8G/wn//f7y/gD///4F/xj/Gv8c/yH/I/8m/0T/WP9S/2X/aP9e/2H/aP9v/3L/eP99/4b/jv+j/7f/vf/b/+X/6/8IAAwAEQAfACQAKQA3AEwATwBZAHYAfwB5AHIAdgCBAHwAewCHAJkAqACjAJkAlwCiAKEAjgCYAJ0AnQCxALsAwADDAMoA1wDVANQA3ADlAO8A/AAIAQYB/QD3APwAAQH9APsA+QD5APEA5wDZAL4ArQCrALMAswCsAKcAmgCbAJ8AlACbAJ4AnwCrAKsApgCjAKYApACgAJ4AmQCYAIsAkgCUAIMAjQCLAA==",
"ewBwAHEAdQBnAF4ATgA8AD4ALgAgAB4ADwAQAAcA/P8BAPr/9f/w/+T/4v/i/+T/7v/t/93/2//h/9v/z//M/8z/xP+8/7H/qP+q/6H/mf+X/4v/ef9v/3b/Zv9b/2P/WP9T/0z/QP9F/z//Q/9O/1D/XP9V/03/VP9R/1r/Vv9G/03/V/9Z/17/ZP9j/2X/b/9z/23/a/9z/3z/gP+D/3//dP91/27/Zv90/3n/d/94/3n/gP+M/5L/mv+l/6n/pf+l/6v/pf+j/6z/tf++/7//xP/M/8H/vf/D/7z/q/+c/6//yP/M/9P/2//l//H/6//m//b/CAAGAAQAGwArAA==",
"MAA2ACwAJQAgABwAKwAwADIAPwBRAFEAPgBEAEIAOgA9ADoARwBTAF8AZgBnAGoAbABvAGsAcwCCAIUAkACQAI0AkgCPAIcAeAByAIIAiwCGAHYAbwB4AHIAcwCJAIQAeQCJAIcAfACAAH0AgQCDAH4AjACNAIkAlgCEAHcAgwCAAHwAfACEAIkAdwB3AHoAagBjAGwAaQBcAF8AZgBlAFgAVgBVAD0AQABGADoAOgA6ADEAMgAxACkAJgAbABQAEwAKAA0ADQAQABcABAD6//n/8//1/+7/6//p/+r/9v/v/+P/5v/g/9//2v/V/+H/3P/e/+f/2v/a/9r/z//H/w==",
"v//C/7b/p/+u/63/qP+n/6H/lv+U/5n/kv+Q/5D/hv97/3b/ff9+/3T/cP9x/2b/Yf9q/2n/bP9z/3P/cP9n/2X/af9j/2D/Zv9r/2//bv97/4n/gf97/3X/cv94/3D/bf94/4P/hf95/3X/gP+C/3X/df98/3r/hP+J/43/l/+O/5T/n/+d/6P/pv+w/8P/0P/a/9z/0v/W/+D/4f/t/+v/7f/4//L/8//u/+D/3f/l/+7/8P/6//n/8f/9//j/9v/+//n/AwAHAAcACgAQABkAHAAlACwANQAyADcAUgBMAE8AZwBiAFoAYQBsAHIAeQB+AHsAfwB+AHoAggCAAA==",
"ggCKAIYAiwCVAJkAlwCWAJoAmwCbAKQAswCzALEAvQDBALwAvQDEAMEAvwC9ALMAtwC3ALIAuAC2ALAArACtAK8ApwCnAKYAogCeAJgAmgCUAIsAjwCOAI0AiQCBAIEAdwB4AHwAbABlAGQAYwBhAF0AWgBaAFsAWgBaAE8ASwBNAEYARABFADkAMwAyAB8AHQAjABkAEAAJAAIABQAGAPz/AAADAPj/7f/p/+X/2v/V/9L/0f/K/8T/xP+4/6//rf+l/53/jv+G/5L/k/+O/4//j/+O/4n/ff95/4L/e/9p/2//dP9z/3n/ef9z/3H/a/9q/2//aP9o/3X/dv9u/w==",
"cv95/3T/cP9v/27/cP9y/3L/d/95/3n/ff93/3//iv+I/4j/iv+I/4r/k/+R/47/kf+Q/5v/n/+V/5T/lv+c/5b/l/+m/6P/rP+y/6z/tv+5/7X/vf/B/8X/0f/T/9j/1//V/9z/3//l/+P/5P/q/+b/7f/4//r/9/////r/8/8FAP//9/8HAAAA+P/7////AQAFAAoABQAEAAcABgANAAoACQATABUAGgAfACwAKgAhACsAKQArADEAMAA2ADQAMQA0ADEANABDAEcARgBJAEsASABEAEUARwBBAD4ASQBLAEEAPABEAEMAQABJAEYARQBJAEkATABNAE4ASQBHAA==",
],
utterance_idx: 0,
text: "Hey there!",
},
{
message: "audio",
audio: [
"SABEAEkASgBJAEwASABEAEkATQBJAE0AVABNAFAAUwBIAE0ASwBEAEkARABGAEoAPgA9AEMAQgA8ADIANgA8ADgAOgA7ADwAPQA3ADYAOAAzADEALgArACoAKgAnACQAIwAdABUAFQAYABYAGAAbABMADgAKAAMABAD///v//v////v/+v8AAPv/9v/z/+f/7P/u/+r/6P/l/+b/5f/j/+L/5P/e/9b/2//W/9T/2P/R/9P/0P/M/8//x//H/8j/vP+6/7j/uf++/7r/u/+5/7X/sv+x/7L/r/+3/7T/p/+p/6r/rP+v/7D/r/+w/7n/uP+4/7n/tf+2/7T/uv+6/w==",
"s/+9/73/tf+6/77/vP+7/7v/tP+w/7P/sv+x/7f/v/+8/7b/vP++/7T/t//E/8j/xP/C/8P/wf/B/8b/yv/Q/9X/0f/S/9f/1P/W/9j/zv/S/+D/3//k/+f/5P/v//P/9P/8//r/+v8CAAYABwALAAwADQAPAAsAFQAeAB0AIQAaABsAKAAkACcALAApACsALQAsACsAMAAwADEANgA2ADoAPAA4ADsAOgA2ADkAPQBBAEUASwBOAEwAVQBZAFUAVgBaAGEAXABaAGAAXABkAGUAXwBhAF4AXwBjAGQAYgBfAGAAYQBZAFoAXABcAFwAWABaAFkAVABUAFIAUABRAA==",
"UQBPAFIAUQBOAFAATQBIAEoARwBDAEAAQABHAEEAPAA6ADYANwA0ADMALwAtACsAIwAjACMAIQAbABcAHAATAA0AFgATAA8AEAAHAAMABgAHAP//AAABAPf//v/8//H/8v/v/+3/6f/c/+L/5v/W/9z/4P/Z/9f/zP/O/8//yv/K/8H/wP/A/7v/xP/D/7n/uf+y/6//uP+x/6z/tv+y/6b/qv+s/6n/qv+u/7X/s/+v/7D/rP+y/7f/tP+3/7r/u//C/8P/wP/C/8T/yP/H/8X/x//E/8b/zf/L/8r/0f/R/9D/0f/U/9z/4P/Z/9j/2//Y/+D/4P/a/9//3f/c/w==",
"4//h/+P/6f/u/+//7P/s/+3/8P/x//H/+v/2//T/AQAAAAIABgAFAAcABgAFAAkADwARAA8ADgARABkAFwAaACAAFwAbACAAHAAgAB8AJQAmACQAJwAlACgAKwAsACoAKQAyACwAKgAzADEANAAyAC4AMAAsACwAKwApACUAJQApACYAIgAkACYAJQAjACQAIAAfACEAIAAeABoAGAAaABsAGQAZABgAEAAQABcAFwATABcAFgAMAA0AEAALAA0ADQAKAA0ACwAFAAIA/v8CAAMA//8CAAMABAADAPz/+P/9//3/+P/2//T/9//5//n/9v/z//P/8f/x//D/7//y/w==",
"8f/v//L/9P/r/+z/7f/n/+n/5//q/+v/5f/m/+f/6v/p/+L/5v/m/+P/6//n/+b/6v/o/+z/7v/r/+3/8P/x/+//7f/t//H/7v/q//H/9P/s/+7/8P/u//L/9f/1//H/8f/x//P/+P/y//H/9//2//X/9P/z//f/9//5//r/+f/4//j//f/8//f/+P////3///8GAP//BwAJAP3/AwADAAEAAAD9/wEAAQD+/wEAAwAAAAEAAwAEAAQAAQADAAIA/v8CAAcACgAGAAMABwAHAAQABwAHAAkADgALAAsADQALAA0AEgASABAADQAOABIADgAPABMAEQATABIADwATAA==",
"EgAPAA8AEgATABEAEwATAA4ADwATABYAEwAOAA0ADgATABEADwAUAA8ACwAMAAwADQAOAAsADwAPAAgACQAJAAMABQAJAAcABgAIAAkACQAEAAYACAACAAEAAgD9//7/AgD///z/+v/5//z/+P/0//j/9f/2//f/8//1//L/7//v//L/8P/o/+z/7//r/+n/6//t/+n/6P/q/+j/6P/p/+b/5//q/+f/4v/n/+z/5//n/+n/5//k/+L/6f/p/+L/6P/m/+T/6f/m/+j/6f/h/+T/6P/o/+3/7f/q/+z/7v/s//D/9f/v/+//8//x//L/8P/z//T/8//4//n/9//4/w==",
],
utterance_idx: 0,
text: "Hey there!",
},
{
message: "audio",
audio: [
"9v/1//b/7v/w//f/8//0//L/8//3//H/8v/1//H/8//z//H/9P/0//P/9P/x//D/8//y//L/9f/w//L/9//1//P/9v/4//r//P/7//v//f///wIAAwACAAUABgADAAcABwAGAAwACwALAAoACgAOAA0ADwAPAA0ADwAOAA8AEQASABMAEgAVABMAEAAUABgAGQAWABYAGQAZABsAHAAcAB0AHAAiACAAHAAgACAAHgAhACMAJQAlACQAJwAkACEAIQAgACYAJQAjACYAJgAnACgAJQAkACcAKAAmACQAIwAkACYAJQAmACUAIAAgAB8AIwAiAB0AIAAfABsAGwAYAA==",
"GAAbABkAGAAbABUAFAATABIAEAAQABMADAANABEACwANAAwABgAIAAYABAAEAPz/AAAGAPn/+/8BAPn/+v/7//f/+P/3//b/8//y//P/8v/z/+7/7v/s/+b/6P/l/+L/5f/j/+P/4P/d/9z/2P/W/9b/1v/W/9L/0v/T/9L/0f/O/8//zv/J/8v/yf/J/8//x//F/83/xP/F/8z/wv/F/8b/wv/I/8X/xP/I/8T/xv/C/8L/xP/B/8T/xf/H/8P/wP/I/8b/xv/G/8f/zP/E/8T/yP/G/8X/w//J/87/yf/K/87/0P/Q/9L/1P/S/9T/2P/Y/9r/2//c/+H/4//j/w==",
"5P/k/+T/6P/r/+z/7//w/+//8f/2//j//P/9//7/AwABAAMABwAEAAoAEgAOABMAFwAXAB4AHAAbACIAIwAkACgAKQAqACwALgAxADIAMgAxADYAOgA5ADoAPgA+ADwAPAA8AEEARgA+AD0AQAA8AD4AQQA/AD8APwA/AEAAPAA7ADwANgA2ADwANwAyADUANQA1ADMAMAAzADAALAAvAC0AKgArACkAKwArACcAKgAnACMAKAAlACMAIgAdAB4AIQAeABsAHAAaABQAEwAVABQAEAAOAA4ADAAIAAQAAwABAPz//f/8//v/+v/2//r/9f/s//P/8v/r/+n/6f/q/w==",
"6f/n/+P/4P/j/+H/4f/f/9r/3f/a/9v/4P/e/9z/3v/c/9z/3//d/9z/4P/f/97/4P/e/9//5//n/+P/5P/l/+f/6//x//H/8v/z//T/+v/4//b/9//1//b/9v/3//j/9//6//v/+//+//////8AAP///v8EAAMAAwAFAAEABAAFAAQABAADAAQABAAIAAkABAAGAAgABwAJAAgABQAEAAMABQAFAAEA/f/+/wEAAAD//wEAAwAAAAAAAgD8//7/AAD6//r/+v/8//r/9//6//f/8//2//T/9P/2//L/8P/z//D/8P/x/+//8v/v/+//8v/w/+//7f/v//L/8P/z/w==",
"9P/z//X/9f/z//P/9f/2//T/9P/1//T/9v/1//X/9v/1//X/9f/3//j/+v/7//j/9//5//z//v/9//7//v/9////AQACAAQABgAFAAQAAwADAAMABQAGAAUABQAHAAkADgALAAgADAAOAA8ADwAOAA4ADAAPABAADAAMAAwADgAOAA4AEQASAA8ACgAMAAkACwAaAA0A/f8CAAIAAAAGAAUABAAIAAYABQAFAAIAAgD/////AAD+////AgAEAAIAAAD///7//v/+//z//P/8//3//f/9//z//f/+//7//f/8//7////9//3//P/9//7//v/+///////+/////////w==",
"//8AAAEAAAAAAAAA//8AAAEAAAAAAAAAAAABAAEAAAAAAAAAAAAAAAAAAQAAAAAAAQAAAAAAAAAAAAAAAAAAAAAA//////////8AAP//////////AAAAAP//AAAAAAEAAAAAAAAAAAAAAAAAAAABAAEAAAABAAAAAAABAAAAAQABAAAAAQAAAAAAAAAAAAAAAAABAAEAAQAAAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAAAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAA==",
],
utterance_idx: 0,
text: "Hey there!",
},
{
message: "audio",
audio: [
"AAABAAEAAQABAAEAAQABAAEAAwACAAIAAwACAAIAAgABAAIAAQABAAIABQAGAAQAAwAEAAQABAAEAAMAAwADAAMAAwADAAMAAgACAAIAAgACAAEAAQACAAEAAQACAAIAAQABAAEAAgABAAIAAgABAAIAAQABAAIAAgABAAIAAgACAAIAAQABAAIAAgACAAEAAQACAAIAAgACAAIAAgACAAEAAQACAAIAAQABAAIAAgACAAIAAgACAAIAAQABAAEAAQACAAIAAQABAAEAAgACAAEAAQACAAIAAQABAAIAAQACAAIAAQABAAIAAgACAAIAAgABAAEAAgACAAIAAgABAA==",
"AQABAAIAAQABAAEAAgABAAIAAQABAAEAAQACAAIAAQABAAEAAgABAAEAAgACAAIAAgACAAEAAQABAAIAAgACAAEAAQABAAIAAgACAAEAAgACAAEAAQACAAEAAQABAAEAAQABAAEAAQACAAEAAQABAAEAAQABAAEAAQABAAEAAgACAAEAAQABAAIAAgABAAIAAQABAAIAAgABAAIAAgABAAEAAgABAAIAAgACAAIAAgACAAIAAgABAAEAAgACAAEAAQACAAIAAgABAAIAAgABAAEAAQABAAIAAQABAAEAAgABAAEAAgACAAIAAgABAAEAAgABAAEAAgABAAEAAgABAA==",
"AQACAAIAAQABAAIAAQABAAIAAQACAAIAAQACAAIAAgABAAEAAgABAAEAAQABAAIAAgABAAIAAQABAAEAAQABAAEAAQABAAEAAQACAAEAAQABAAIAAgABAAEAAQABAAEAAQACAAEAAQABAAEAAQABAAEAAQABAAEAAAAAAAEAAQABAAEAAQABAAEAAQABAAEAAgACAAEAAgACAAEAAgACAAIAAgABAAEAAQACAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAAAAAAAAAAAAAAAQAAAAEAAQABAAEA/////wAAAAAAAAAAAAAAAAAAAAAAAA==",
],
utterance_idx: 0,
text: "Hey there!",
},
{
message: "audio",
audio: [
"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",
],
utterance_idx: 0,
text: "Hey there!",
},
{
message: "audio",
audio: [
"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",
],
utterance_idx: 0,
text: "Hey there!",
},
{
message: "audio",
audio: [
"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",
],
utterance_idx: 0,
text: "Hey there!",
},
{
message: "audio",
audio: [
"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",
],
utterance_idx: 0,
text: "Hey there!",
},
{
message: "audio",
audio: [
"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",
],
utterance_idx: 0,
text: "Hey there!",
},
{
message: "audio",
audio: [
"Au3S8A72kfvR/f79//5HAF8BogL5A3gGBgknCsEM2w0dDWUNPwupCaAJeghSDZ0S1hGuEzkUvxVrGzAaaBvcIRwjOSj8KzosOTR6MLIhkBsVFzkcByLlF1AZAh8pGN4RywQs/f3/q/FP59TuP/Pg+R/1S+qN8aPv+OVG507nee8W+DH2jwJpDoAOvRLREAYRFRXQD0gVCyHFIaQjciORI8YmnhtJEY0Q5wooBlgBW/+TBQ8CAPnz9UTwx+rb5PreBuJL46Hhfua76gPuU+8E7P7sQ+7/7NTwS/bN+z4BUQL2AhsEHgKN/5D82fj1+G76gvrK+gX4WPSN8Gbp5uSP4g==",
"dd/k3tzd/9303ybeSt363cfcJN1X3KLcLOLd5fvpKPBj8ij0XvRD9Lv6r/8CATYCpgE5A2sEnQOJBeIIlAoDCjYI/gcuCpgLuAjKBBgGiAreEA8V0xTCFncY/RdAGZgc5yE9KYEtMi/cM8I3+jS8KUcalBODGkEedRrWGAIZEho7EIb84fX58yrqIeKR3gTpmPZc8FrrSO0g64zrG+YT59729/ypAmkOxBX5H0YhFxsfHrwbIxdIHNAg2yZTKAIiLySvI5UVqgbA+8b4T/hj8ATvIvcX+XTz9Og/4VXhS9002J/aa+HU7PD1EvpV/nH+oPp29vDzIfi+/+YFHAuZDg==",
"yBBgELkLvQSk+3rzoPAk8uP0HfUG9I3yzexv4v3YaNVt1fXUL9QD2N/egeLr4Qbej9yh3czdQeBl5VfucPhf/RAAlQHYAGH//fyZ/dIBQQY9CSMKNQp+B4gFMARZAJQA7/+4/5MCAwHGBIYJ1AcQCYsJbQwqEuASWRYDHHgediHgJdosyzQFOFk3Pjm/OpE0miaqF/IUTBs4G90UixLjEyIRXgKv75DpQuTH2uHWB9y061v1aPEo8Efx0+5l7ADpXe89AFsMXRfrIuspNC9qLGgjhh9dG9wYAR5jIgEkXCLDHDMZzxCN/6vxJ+om5ZLi+eI26ZLwPO6M5ibiyd/R3Q==",
"D9z63c3msvFO+qICzwpIDj0LKAW5AfMDIAikCk8OnBJwEyMRYgqvAZb6yvG86jrn+eUm6f7qrefr4dXae9Wv0iXQy9Bp1EHXodyJ42HoXOv46f7o++nq5wfqsPER+eT+kP5f/b7/e/6j/Av8APuI+7n6IvpH+977svuy+sP5A/mO+Xj7bfwX/Jv7hvtx/moDCAZuCTsO0xNHG60ehB+WIvkjsSQUJ50r/DM7PKc9eTssPKc1gic6FfsF6AtUEncMPQhjBXsG/QB362zesN3h1hLUxthw5Vb59Pvz9n76z/je9i73sPjhBjMWVx5WKWwxUzLKL1UlaBpDFgYQkg0wEw==",
"EBVRFPYPXwhRBHz41+Wt24nZldqb3RTiq+uV9GLxrOrT6K3pe+xm7sDzvADWDYgWLBvMGzcYJBDGBu4BoAPlBtMI6AjtBogC2vol80TrMOJo2q/W6NiA3cDhCOU35ebgtNr9113Z2N1L45bo6u3s8bz1EPl0+bD17vAI7lDuzfK290f8Zv41+yz3O/Pl8O3wve6R61Tp4unA7UXxavS39uH34fdc9//5Jv3X/4sDLgaMCncOUxJYFxEWYhPEE4cXPx4oIC8hGiRTJIYjOyPPJB8o5ildKK8poCzZLHwndRMgAaj+DwY5DWAHp/91AUYAw/UM6wTlaOOv4W3f9Om0/A==",
"CghiDLkH1/+T/iT+tQLHDIcUQR7XJkUr5S2gKCIdFROZCY4CnQPMCjYSQxEmBpL95/lm8oPnnt7Z3PDhfufl7eH3D/5T/J72WvDu8Az3wf3+Bq4NaBKxGPoc+Bx3FCkHTv52+438Xf5gAFMBCP/y+JzvEOhm4x3getyd2LXbaeXK7U7u8Oc55KTjS+Mk5ATmh+sK8GHyq/eP+3/8xfjL8GHs7et07ufzFPia+Qb6xfi697b2/vHq7c/rd+qt64DuH/TM+UP7Gvob+nD7OftL+/D7iv5AAzUGswrpDvsPgA6NCbEFNQZ+CvAOdhE8E9kVCxoKHBcc/xzkHB0dCB/LIQ==",
],
utterance_idx: 0,
text: "Hey there!",
},
{
message: "audio",
audio: [
"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",
],
utterance_idx: 0,
text: "Hey there!",
},
{
message: "audio",
audio: [
"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",
],
utterance_idx: 0,
text: "Hey there!",
},
{
message: "audio",
audio: [
"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",
],
utterance_idx: 0,
text: "Hey there!",
},
{
message: "audio",
audio: [
"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",
],
utterance_idx: 0,
text: "Hey there!",
},
{
message: "audio",
audio: [
"wP7V/ur++v4T/xb/Ef8U/xX/Dv8G/wn//f7y/gD///4F/xj/Gv8c/yH/I/8m/0T/WP9S/2X/aP9e/2H/aP9v/3L/eP99/4b/jv+j/7f/vf/b/+X/6/8IAAwAEQAfACQAKQA3AEwATwBZAHYAfwB5AHIAdgCBAHwAewCHAJkAqACjAJkAlwCiAKEAjgCYAJ0AnQCxALsAwADDAMoA1wDVANQA3ADlAO8A/AAIAQYB/QD3APwAAQH9APsA+QD5APEA5wDZAL4ArQCrALMAswCsAKcAmgCbAJ8AlACbAJ4AnwCrAKsApgCjAKYApACgAJ4AmQCYAIsAkgCUAIMAjQCLAA==",
"ewBwAHEAdQBnAF4ATgA8AD4ALgAgAB4ADwAQAAcA/P8BAPr/9f/w/+T/4v/i/+T/7v/t/93/2//h/9v/z//M/8z/xP+8/7H/qP+q/6H/mf+X/4v/ef9v/3b/Zv9b/2P/WP9T/0z/QP9F/z//Q/9O/1D/XP9V/03/VP9R/1r/Vv9G/03/V/9Z/17/ZP9j/2X/b/9z/23/a/9z/3z/gP+D/3//dP91/27/Zv90/3n/d/94/3n/gP+M/5L/mv+l/6n/pf+l/6v/pf+j/6z/tf++/7//xP/M/8H/vf/D/7z/q/+c/6//yP/M/9P/2//l//H/6//m//b/CAAGAAQAGwArAA==",
"MAA2ACwAJQAgABwAKwAwADIAPwBRAFEAPgBEAEIAOgA9ADoARwBTAF8AZgBnAGoAbABvAGsAcwCCAIUAkACQAI0AkgCPAIcAeAByAIIAiwCGAHYAbwB4AHIAcwCJAIQAeQCJAIcAfACAAH0AgQCDAH4AjACNAIkAlgCEAHcAgwCAAHwAfACEAIkAdwB3AHoAagBjAGwAaQBcAF8AZgBlAFgAVgBVAD0AQABGADoAOgA6ADEAMgAxACkAJgAbABQAEwAKAA0ADQAQABcABAD6//n/8//1/+7/6//p/+r/9v/v/+P/5v/g/9//2v/V/+H/3P/e/+f/2v/a/9r/z//H/w==",
"v//C/7b/p/+u/63/qP+n/6H/lv+U/5n/kv+Q/5D/hv97/3b/ff9+/3T/cP9x/2b/Yf9q/2n/bP9z/3P/cP9n/2X/af9j/2D/Zv9r/2//bv97/4n/gf97/3X/cv94/3D/bf94/4P/hf95/3X/gP+C/3X/df98/3r/hP+J/43/l/+O/5T/n/+d/6P/pv+w/8P/0P/a/9z/0v/W/+D/4f/t/+v/7f/4//L/8//u/+D/3f/l/+7/8P/6//n/8f/9//j/9v/+//n/AwAHAAcACgAQABkAHAAlACwANQAyADcAUgBMAE8AZwBiAFoAYQBsAHIAeQB+AHsAfwB+AHoAggCAAA==",
"ggCKAIYAiwCVAJkAlwCWAJoAmwCbAKQAswCzALEAvQDBALwAvQDEAMEAvwC9ALMAtwC3ALIAuAC2ALAArACtAK8ApwCnAKYAogCeAJgAmgCUAIsAjwCOAI0AiQCBAIEAdwB4AHwAbABlAGQAYwBhAF0AWgBaAFsAWgBaAE8ASwBNAEYARABFADkAMwAyAB8AHQAjABkAEAAJAAIABQAGAPz/AAADAPj/7f/p/+X/2v/V/9L/0f/K/8T/xP+4/6//rf+l/53/jv+G/5L/k/+O/4//j/+O/4n/ff95/4L/e/9p/2//dP9z/3n/ef9z/3H/a/9q/2//aP9o/3X/dv9u/w==",
"cv95/3T/cP9v/27/cP9y/3L/d/95/3n/ff93/3//iv+I/4j/iv+I/4r/k/+R/47/kf+Q/5v/n/+V/5T/lv+c/5b/l/+m/6P/rP+y/6z/tv+5/7X/vf/B/8X/0f/T/9j/1//V/9z/3//l/+P/5P/q/+b/7f/4//r/9/////r/8/8FAP//9/8HAAAA+P/7////AQAFAAoABQAEAAcABgANAAoACQATABUAGgAfACwAKgAhACsAKQArADEAMAA2ADQAMQA0ADEANABDAEcARgBJAEsASABEAEUARwBBAD4ASQBLAEEAPABEAEMAQABJAEYARQBJAEkATABNAE4ASQBHAA==",
],
utterance_idx: 0,
text: "Hey there!",
},
{
message: "audio",
audio: [
"SABEAEkASgBJAEwASABEAEkATQBJAE0AVABNAFAAUwBIAE0ASwBEAEkARABGAEoAPgA9AEMAQgA8ADIANgA8ADgAOgA7ADwAPQA3ADYAOAAzADEALgArACoAKgAnACQAIwAdABUAFQAYABYAGAAbABMADgAKAAMABAD///v//v////v/+v8AAPv/9v/z/+f/7P/u/+r/6P/l/+b/5f/j/+L/5P/e/9b/2//W/9T/2P/R/9P/0P/M/8//x//H/8j/vP+6/7j/uf++/7r/u/+5/7X/sv+x/7L/r/+3/7T/p/+p/6r/rP+v/7D/r/+w/7n/uP+4/7n/tf+2/7T/uv+6/w==",
"s/+9/73/tf+6/77/vP+7/7v/tP+w/7P/sv+x/7f/v/+8/7b/vP++/7T/t//E/8j/xP/C/8P/wf/B/8b/yv/Q/9X/0f/S/9f/1P/W/9j/zv/S/+D/3//k/+f/5P/v//P/9P/8//r/+v8CAAYABwALAAwADQAPAAsAFQAeAB0AIQAaABsAKAAkACcALAApACsALQAsACsAMAAwADEANgA2ADoAPAA4ADsAOgA2ADkAPQBBAEUASwBOAEwAVQBZAFUAVgBaAGEAXABaAGAAXABkAGUAXwBhAF4AXwBjAGQAYgBfAGAAYQBZAFoAXABcAFwAWABaAFkAVABUAFIAUABRAA==",
"UQBPAFIAUQBOAFAATQBIAEoARwBDAEAAQABHAEEAPAA6ADYANwA0ADMALwAtACsAIwAjACMAIQAbABcAHAATAA0AFgATAA8AEAAHAAMABgAHAP//AAABAPf//v/8//H/8v/v/+3/6f/c/+L/5v/W/9z/4P/Z/9f/zP/O/8//yv/K/8H/wP/A/7v/xP/D/7n/uf+y/6//uP+x/6z/tv+y/6b/qv+s/6n/qv+u/7X/s/+v/7D/rP+y/7f/tP+3/7r/u//C/8P/wP/C/8T/yP/H/8X/x//E/8b/zf/L/8r/0f/R/9D/0f/U/9z/4P/Z/9j/2//Y/+D/4P/a/9//3f/c/w==",
"4//h/+P/6f/u/+//7P/s/+3/8P/x//H/+v/2//T/AQAAAAIABgAFAAcABgAFAAkADwARAA8ADgARABkAFwAaACAAFwAbACAAHAAgAB8AJQAmACQAJwAlACgAKwAsACoAKQAyACwAKgAzADEANAAyAC4AMAAsACwAKwApACUAJQApACYAIgAkACYAJQAjACQAIAAfACEAIAAeABoAGAAaABsAGQAZABgAEAAQABcAFwATABcAFgAMAA0AEAALAA0ADQAKAA0ACwAFAAIA/v8CAAMA//8CAAMABAADAPz/+P/9//3/+P/2//T/9//5//n/9v/z//P/8f/x//D/7//y/w==",
"8f/v//L/9P/r/+z/7f/n/+n/5//q/+v/5f/m/+f/6v/p/+L/5v/m/+P/6//n/+b/6v/o/+z/7v/r/+3/8P/x/+//7f/t//H/7v/q//H/9P/s/+7/8P/u//L/9f/1//H/8f/x//P/+P/y//H/9//2//X/9P/z//f/9//5//r/+f/4//j//f/8//f/+P////3///8GAP//BwAJAP3/AwADAAEAAAD9/wEAAQD+/wEAAwAAAAEAAwAEAAQAAQADAAIA/v8CAAcACgAGAAMABwAHAAQABwAHAAkADgALAAsADQALAA0AEgASABAADQAOABIADgAPABMAEQATABIADwATAA==",
"EgAPAA8AEgATABEAEwATAA4ADwATABYAEwAOAA0ADgATABEADwAUAA8ACwAMAAwADQAOAAsADwAPAAgACQAJAAMABQAJAAcABgAIAAkACQAEAAYACAACAAEAAgD9//7/AgD///z/+v/5//z/+P/0//j/9f/2//f/8//1//L/7//v//L/8P/o/+z/7//r/+n/6//t/+n/6P/q/+j/6P/p/+b/5//q/+f/4v/n/+z/5//n/+n/5//k/+L/6f/p/+L/6P/m/+T/6f/m/+j/6f/h/+T/6P/o/+3/7f/q/+z/7v/s//D/9f/v/+//8//x//L/8P/z//T/8//4//n/9//4/w==",
],
utterance_idx: 0,
text: "Hey there!",
},
{
message: "audio",
audio: [
"9v/1//b/7v/w//f/8//0//L/8//3//H/8v/1//H/8//z//H/9P/0//P/9P/x//D/8//y//L/9f/w//L/9//1//P/9v/4//r//P/7//v//f///wIAAwACAAUABgADAAcABwAGAAwACwALAAoACgAOAA0ADwAPAA0ADwAOAA8AEQASABMAEgAVABMAEAAUABgAGQAWABYAGQAZABsAHAAcAB0AHAAiACAAHAAgACAAHgAhACMAJQAlACQAJwAkACEAIQAgACYAJQAjACYAJgAnACgAJQAkACcAKAAmACQAIwAkACYAJQAmACUAIAAgAB8AIwAiAB0AIAAfABsAGwAYAA==",
"GAAbABkAGAAbABUAFAATABIAEAAQABMADAANABEACwANAAwABgAIAAYABAAEAPz/AAAGAPn/+/8BAPn/+v/7//f/+P/3//b/8//y//P/8v/z/+7/7v/s/+b/6P/l/+L/5f/j/+P/4P/d/9z/2P/W/9b/1v/W/9L/0v/T/9L/0f/O/8//zv/J/8v/yf/J/8//x//F/83/xP/F/8z/wv/F/8b/wv/I/8X/xP/I/8T/xv/C/8L/xP/B/8T/xf/H/8P/wP/I/8b/xv/G/8f/zP/E/8T/yP/G/8X/w//J/87/yf/K/87/0P/Q/9L/1P/S/9T/2P/Y/9r/2//c/+H/4//j/w==",
"5P/k/+T/6P/r/+z/7//w/+//8f/2//j//P/9//7/AwABAAMABwAEAAoAEgAOABMAFwAXAB4AHAAbACIAIwAkACgAKQAqACwALgAxADIAMgAxADYAOgA5ADoAPgA+ADwAPAA8AEEARgA+AD0AQAA8AD4AQQA/AD8APwA/AEAAPAA7ADwANgA2ADwANwAyADUANQA1ADMAMAAzADAALAAvAC0AKgArACkAKwArACcAKgAnACMAKAAlACMAIgAdAB4AIQAeABsAHAAaABQAEwAVABQAEAAOAA4ADAAIAAQAAwABAPz//f/8//v/+v/2//r/9f/s//P/8v/r/+n/6f/q/w==",
"6f/n/+P/4P/j/+H/4f/f/9r/3f/a/9v/4P/e/9z/3v/c/9z/3//d/9z/4P/f/97/4P/e/9//5//n/+P/5P/l/+f/6//x//H/8v/z//T/+v/4//b/9//1//b/9v/3//j/9//6//v/+//+//////8AAP///v8EAAMAAwAFAAEABAAFAAQABAADAAQABAAIAAkABAAGAAgABwAJAAgABQAEAAMABQAFAAEA/f/+/wEAAAD//wEAAwAAAAAAAgD8//7/AAD6//r/+v/8//r/9//6//f/8//2//T/9P/2//L/8P/z//D/8P/x/+//8v/v/+//8v/w/+//7f/v//L/8P/z/w==",
"9P/z//X/9f/z//P/9f/2//T/9P/1//T/9v/1//X/9v/1//X/9f/3//j/+v/7//j/9//5//z//v/9//7//v/9////AQACAAQABgAFAAQAAwADAAMABQAGAAUABQAHAAkADgALAAgADAAOAA8ADwAOAA4ADAAPABAADAAMAAwADgAOAA4AEQASAA8ACgAMAAkACwAaAA0A/f8CAAIAAAAGAAUABAAIAAYABQAFAAIAAgD/////AAD+////AgAEAAIAAAD///7//v/+//z//P/8//3//f/9//z//f/+//7//f/8//7////9//3//P/9//7//v/+///////+/////////w==",
"//8AAAEAAAAAAAAA//8AAAEAAAAAAAAAAAABAAEAAAAAAAAAAAAAAAAAAQAAAAAAAQAAAAAAAAAAAAAAAAAAAAAA//////////8AAP//////////AAAAAP//AAAAAAEAAAAAAAAAAAAAAAAAAAABAAEAAAABAAAAAAABAAAAAQABAAAAAQAAAAAAAAAAAAAAAAABAAEAAQAAAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAAAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAA==",
],
utterance_idx: 0,
text: "Hey there!",
},
{
message: "audio",
audio: [
"AAABAAEAAQABAAEAAQABAAEAAwACAAIAAwACAAIAAgABAAIAAQABAAIABQAGAAQAAwAEAAQABAAEAAMAAwADAAMAAwADAAMAAgACAAIAAgACAAEAAQACAAEAAQACAAIAAQABAAEAAgABAAIAAgABAAIAAQABAAIAAgABAAIAAgACAAIAAQABAAIAAgACAAEAAQACAAIAAgACAAIAAgACAAEAAQACAAIAAQABAAIAAgACAAIAAgACAAIAAQABAAEAAQACAAIAAQABAAEAAgACAAEAAQACAAIAAQABAAIAAQACAAIAAQABAAIAAgACAAIAAgABAAEAAgACAAIAAgABAA==",
"AQABAAIAAQABAAEAAgABAAIAAQABAAEAAQACAAIAAQABAAEAAgABAAEAAgACAAIAAgACAAEAAQABAAIAAgACAAEAAQABAAIAAgACAAEAAgACAAEAAQACAAEAAQABAAEAAQABAAEAAQACAAEAAQABAAEAAQABAAEAAQABAAEAAgACAAEAAQABAAIAAgABAAIAAQABAAIAAgABAAIAAgABAAEAAgABAAIAAgACAAIAAgACAAIAAgABAAEAAgACAAEAAQACAAIAAgABAAIAAgABAAEAAQABAAIAAQABAAEAAgABAAEAAgACAAIAAgABAAEAAgABAAEAAgABAAEAAgABAA==",
"AQACAAIAAQABAAIAAQABAAIAAQACAAIAAQACAAIAAgABAAEAAgABAAEAAQABAAIAAgABAAIAAQABAAEAAQABAAEAAQABAAEAAQACAAEAAQABAAIAAgABAAEAAQABAAEAAQACAAEAAQABAAEAAQABAAEAAQABAAEAAAAAAAEAAQABAAEAAQABAAEAAQABAAEAAgACAAEAAgACAAEAAgACAAIAAgABAAEAAQACAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAAAAAAAAAAAAAAAQAAAAEAAQABAAEA/////wAAAAAAAAAAAAAAAAAAAAAAAA==",
],
utterance_idx: 0,
text: "Hey there!",
},
{
message: "audio",
audio: [
"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",
],
utterance_idx: 0,
text: "Hey there!",
},
{
message: "audio",
audio: [
"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",
],
utterance_idx: 0,
text: "Hey there!",
},
{
message: "audio",
audio: [
"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",
],
utterance_idx: 0,
text: "Hey there!",
},
{
message: "audio",
audio: [
"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",
],
utterance_idx: 0,
text: "Hey there!",
},
{
message: "audio",
audio: [
"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",
],
utterance_idx: 0,
text: "Hey there!",
},
{
message: "audio",
audio: [
"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",
],
utterance_idx: 0,
text: "Hey there!",
},
{
message: "audio",
audio: [
"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",
],
utterance_idx: 0,
text: "Hey there!",
},
{
message: "audio",
audio: [
"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",
],
utterance_idx: 0,
text: "Hey there!",
},
{
message: "audio",
audio: [
"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",
],
utterance_idx: 0,
text: "Hey there!",
},
{
message: "audio",
audio: [
"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",
],
utterance_idx: 0,
text: "Hey there!",
},
{
message: "audio",
audio: [
"wP7V/ur++v4T/xb/Ef8U/xX/Dv8G/wn//f7y/gD///4F/xj/Gv8c/yH/I/8m/0T/WP9S/2X/aP9e/2H/aP9v/3L/eP99/4b/jv+j/7f/vf/b/+X/6/8IAAwAEQAfACQAKQA3AEwATwBZAHYAfwB5AHIAdgCBAHwAewCHAJkAqACjAJkAlwCiAKEAjgCYAJ0AnQCxALsAwADDAMoA1wDVANQA3ADlAO8A/AAIAQYB/QD3APwAAQH9APsA+QD5APEA5wDZAL4ArQCrALMAswCsAKcAmgCbAJ8AlACbAJ4AnwCrAKsApgCjAKYApACgAJ4AmQCYAIsAkgCUAIMAjQCLAA==",
"ewBwAHEAdQBnAF4ATgA8AD4ALgAgAB4ADwAQAAcA/P8BAPr/9f/w/+T/4v/i/+T/7v/t/93/2//h/9v/z//M/8z/xP+8/7H/qP+q/6H/mf+X/4v/ef9v/3b/Zv9b/2P/WP9T/0z/QP9F/z//Q/9O/1D/XP9V/03/VP9R/1r/Vv9G/03/V/9Z/17/ZP9j/2X/b/9z/23/a/9z/3z/gP+D/3//dP91/27/Zv90/3n/d/94/3n/gP+M/5L/mv+l/6n/pf+l/6v/pf+j/6z/tf++/7//xP/M/8H/vf/D/7z/q/+c/6//yP/M/9P/2//l//H/6//m//b/CAAGAAQAGwArAA==",
"MAA2ACwAJQAgABwAKwAwADIAPwBRAFEAPgBEAEIAOgA9ADoARwBTAF8AZgBnAGoAbABvAGsAcwCCAIUAkACQAI0AkgCPAIcAeAByAIIAiwCGAHYAbwB4AHIAcwCJAIQAeQCJAIcAfACAAH0AgQCDAH4AjACNAIkAlgCEAHcAgwCAAHwAfACEAIkAdwB3AHoAagBjAGwAaQBcAF8AZgBlAFgAVgBVAD0AQABGADoAOgA6ADEAMgAxACkAJgAbABQAEwAKAA0ADQAQABcABAD6//n/8//1/+7/6//p/+r/9v/v/+P/5v/g/9//2v/V/+H/3P/e/+f/2v/a/9r/z//H/w==",
"v//C/7b/p/+u/63/qP+n/6H/lv+U/5n/kv+Q/5D/hv97/3b/ff9+/3T/cP9x/2b/Yf9q/2n/bP9z/3P/cP9n/2X/af9j/2D/Zv9r/2//bv97/4n/gf97/3X/cv94/3D/bf94/4P/hf95/3X/gP+C/3X/df98/3r/hP+J/43/l/+O/5T/n/+d/6P/pv+w/8P/0P/a/9z/0v/W/+D/4f/t/+v/7f/4//L/8//u/+D/3f/l/+7/8P/6//n/8f/9//j/9v/+//n/AwAHAAcACgAQABkAHAAlACwANQAyADcAUgBMAE8AZwBiAFoAYQBsAHIAeQB+AHsAfwB+AHoAggCAAA==",
"ggCKAIYAiwCVAJkAlwCWAJoAmwCbAKQAswCzALEAvQDBALwAvQDEAMEAvwC9ALMAtwC3ALIAuAC2ALAArACtAK8ApwCnAKYAogCeAJgAmgCUAIsAjwCOAI0AiQCBAIEAdwB4AHwAbABlAGQAYwBhAF0AWgBaAFsAWgBaAE8ASwBNAEYARABFADkAMwAyAB8AHQAjABkAEAAJAAIABQAGAPz/AAADAPj/7f/p/+X/2v/V/9L/0f/K/8T/xP+4/6//rf+l/53/jv+G/5L/k/+O/4//j/+O/4n/ff95/4L/e/9p/2//dP9z/3n/ef9z/3H/a/9q/2//aP9o/3X/dv9u/w==",
"cv95/3T/cP9v/27/cP9y/3L/d/95/3n/ff93/3//iv+I/4j/iv+I/4r/k/+R/47/kf+Q/5v/n/+V/5T/lv+c/5b/l/+m/6P/rP+y/6z/tv+5/7X/vf/B/8X/0f/T/9j/1//V/9z/3//l/+P/5P/q/+b/7f/4//r/9/////r/8/8FAP//9/8HAAAA+P/7////AQAFAAoABQAEAAcABgANAAoACQATABUAGgAfACwAKgAhACsAKQArADEAMAA2ADQAMQA0ADEANABDAEcARgBJAEsASABEAEUARwBBAD4ASQBLAEEAPABEAEMAQABJAEYARQBJAEkATABNAE4ASQBHAA==",
],
utterance_idx: 0,
text: "Hey there!",
},
{
message: "audio",
audio: [
"SABEAEkASgBJAEwASABEAEkATQBJAE0AVABNAFAAUwBIAE0ASwBEAEkARABGAEoAPgA9AEMAQgA8ADIANgA8ADgAOgA7ADwAPQA3ADYAOAAzADEALgArACoAKgAnACQAIwAdABUAFQAYABYAGAAbABMADgAKAAMABAD///v//v////v/+v8AAPv/9v/z/+f/7P/u/+r/6P/l/+b/5f/j/+L/5P/e/9b/2//W/9T/2P/R/9P/0P/M/8//x//H/8j/vP+6/7j/uf++/7r/u/+5/7X/sv+x/7L/r/+3/7T/p/+p/6r/rP+v/7D/r/+w/7n/uP+4/7n/tf+2/7T/uv+6/w==",
"s/+9/73/tf+6/77/vP+7/7v/tP+w/7P/sv+x/7f/v/+8/7b/vP++/7T/t//E/8j/xP/C/8P/wf/B/8b/yv/Q/9X/0f/S/9f/1P/W/9j/zv/S/+D/3//k/+f/5P/v//P/9P/8//r/+v8CAAYABwALAAwADQAPAAsAFQAeAB0AIQAaABsAKAAkACcALAApACsALQAsACsAMAAwADEANgA2ADoAPAA4ADsAOgA2ADkAPQBBAEUASwBOAEwAVQBZAFUAVgBaAGEAXABaAGAAXABkAGUAXwBhAF4AXwBjAGQAYgBfAGAAYQBZAFoAXABcAFwAWABaAFkAVABUAFIAUABRAA==",
"UQBPAFIAUQBOAFAATQBIAEoARwBDAEAAQABHAEEAPAA6ADYANwA0ADMALwAtACsAIwAjACMAIQAbABcAHAATAA0AFgATAA8AEAAHAAMABgAHAP//AAABAPf//v/8//H/8v/v/+3/6f/c/+L/5v/W/9z/4P/Z/9f/zP/O/8//yv/K/8H/wP/A/7v/xP/D/7n/uf+y/6//uP+x/6z/tv+y/6b/qv+s/6n/qv+u/7X/s/+v/7D/rP+y/7f/tP+3/7r/u//C/8P/wP/C/8T/yP/H/8X/x//E/8b/zf/L/8r/0f/R/9D/0f/U/9z/4P/Z/9j/2//Y/+D/4P/a/9//3f/c/w==",
"4//h/+P/6f/u/+//7P/s/+3/8P/x//H/+v/2//T/AQAAAAIABgAFAAcABgAFAAkADwARAA8ADgARABkAFwAaACAAFwAbACAAHAAgAB8AJQAmACQAJwAlACgAKwAsACoAKQAyACwAKgAzADEANAAyAC4AMAAsACwAKwApACUAJQApACYAIgAkACYAJQAjACQAIAAfACEAIAAeABoAGAAaABsAGQAZABgAEAAQABcAFwATABcAFgAMAA0AEAALAA0ADQAKAA0ACwAFAAIA/v8CAAMA//8CAAMABAADAPz/+P/9//3/+P/2//T/9//5//n/9v/z//P/8f/x//D/7//y/w==",
"8f/v//L/9P/r/+z/7f/n/+n/5//q/+v/5f/m/+f/6v/p/+L/5v/m/+P/6//n/+b/6v/o/+z/7v/r/+3/8P/x/+//7f/t//H/7v/q//H/9P/s/+7/8P/u//L/9f/1//H/8f/x//P/+P/y//H/9//2//X/9P/z//f/9//5//r/+f/4//j//f/8//f/+P////3///8GAP//BwAJAP3/AwADAAEAAAD9/wEAAQD+/wEAAwAAAAEAAwAEAAQAAQADAAIA/v8CAAcACgAGAAMABwAHAAQABwAHAAkADgALAAsADQALAA0AEgASABAADQAOABIADgAPABMAEQATABIADwATAA==",
"EgAPAA8AEgATABEAEwATAA4ADwATABYAEwAOAA0ADgATABEADwAUAA8ACwAMAAwADQAOAAsADwAPAAgACQAJAAMABQAJAAcABgAIAAkACQAEAAYACAACAAEAAgD9//7/AgD///z/+v/5//z/+P/0//j/9f/2//f/8//1//L/7//v//L/8P/o/+z/7//r/+n/6//t/+n/6P/q/+j/6P/p/+b/5//q/+f/4v/n/+z/5//n/+n/5//k/+L/6f/p/+L/6P/m/+T/6f/m/+j/6f/h/+T/6P/o/+3/7f/q/+z/7v/s//D/9f/v/+//8//x//L/8P/z//T/8//4//n/9//4/w==",
],
utterance_idx: 0,
text: "Hey there!",
},
{
message: "audio",
audio: [
"9v/1//b/7v/w//f/8//0//L/8//3//H/8v/1//H/8//z//H/9P/0//P/9P/x//D/8//y//L/9f/w//L/9//1//P/9v/4//r//P/7//v//f///wIAAwACAAUABgADAAcABwAGAAwACwALAAoACgAOAA0ADwAPAA0ADwAOAA8AEQASABMAEgAVABMAEAAUABgAGQAWABYAGQAZABsAHAAcAB0AHAAiACAAHAAgACAAHgAhACMAJQAlACQAJwAkACEAIQAgACYAJQAjACYAJgAnACgAJQAkACcAKAAmACQAIwAkACYAJQAmACUAIAAgAB8AIwAiAB0AIAAfABsAGwAYAA==",
"GAAbABkAGAAbABUAFAATABIAEAAQABMADAANABEACwANAAwABgAIAAYABAAEAPz/AAAGAPn/+/8BAPn/+v/7//f/+P/3//b/8//y//P/8v/z/+7/7v/s/+b/6P/l/+L/5f/j/+P/4P/d/9z/2P/W/9b/1v/W/9L/0v/T/9L/0f/O/8//zv/J/8v/yf/J/8//x//F/83/xP/F/8z/wv/F/8b/wv/I/8X/xP/I/8T/xv/C/8L/xP/B/8T/xf/H/8P/wP/I/8b/xv/G/8f/zP/E/8T/yP/G/8X/w//J/87/yf/K/87/0P/Q/9L/1P/S/9T/2P/Y/9r/2//c/+H/4//j/w==",
"5P/k/+T/6P/r/+z/7//w/+//8f/2//j//P/9//7/AwABAAMABwAEAAoAEgAOABMAFwAXAB4AHAAbACIAIwAkACgAKQAqACwALgAxADIAMgAxADYAOgA5ADoAPgA+ADwAPAA8AEEARgA+AD0AQAA8AD4AQQA/AD8APwA/AEAAPAA7ADwANgA2ADwANwAyADUANQA1ADMAMAAzADAALAAvAC0AKgArACkAKwArACcAKgAnACMAKAAlACMAIgAdAB4AIQAeABsAHAAaABQAEwAVABQAEAAOAA4ADAAIAAQAAwABAPz//f/8//v/+v/2//r/9f/s//P/8v/r/+n/6f/q/w==",
"6f/n/+P/4P/j/+H/4f/f/9r/3f/a/9v/4P/e/9z/3v/c/9z/3//d/9z/4P/f/97/4P/e/9//5//n/+P/5P/l/+f/6//x//H/8v/z//T/+v/4//b/9//1//b/9v/3//j/9//6//v/+//+//////8AAP///v8EAAMAAwAFAAEABAAFAAQABAADAAQABAAIAAkABAAGAAgABwAJAAgABQAEAAMABQAFAAEA/f/+/wEAAAD//wEAAwAAAAAAAgD8//7/AAD6//r/+v/8//r/9//6//f/8//2//T/9P/2//L/8P/z//D/8P/x/+//8v/v/+//8v/w/+//7f/v//L/8P/z/w==",
"9P/z//X/9f/z//P/9f/2//T/9P/1//T/9v/1//X/9v/1//X/9f/3//j/+v/7//j/9//5//z//v/9//7//v/9////AQACAAQABgAFAAQAAwADAAMABQAGAAUABQAHAAkADgALAAgADAAOAA8ADwAOAA4ADAAPABAADAAMAAwADgAOAA4AEQASAA8ACgAMAAkACwAaAA0A/f8CAAIAAAAGAAUABAAIAAYABQAFAAIAAgD/////AAD+////AgAEAAIAAAD///7//v/+//z//P/8//3//f/9//z//f/+//7//f/8//7////9//3//P/9//7//v/+///////+/////////w==",
"//8AAAEAAAAAAAAA//8AAAEAAAAAAAAAAAABAAEAAAAAAAAAAAAAAAAAAQAAAAAAAQAAAAAAAAAAAAAAAAAAAAAA//////////8AAP//////////AAAAAP//AAAAAAEAAAAAAAAAAAAAAAAAAAABAAEAAAABAAAAAAABAAAAAQABAAAAAQAAAAAAAAAAAAAAAAABAAEAAQAAAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAAAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAA==",
],
utterance_idx: 0,
text: "Hey there!",
},
{
message: "audio",
audio: [
"AAABAAEAAQABAAEAAQABAAEAAwACAAIAAwACAAIAAgABAAIAAQABAAIABQAGAAQAAwAEAAQABAAEAAMAAwADAAMAAwADAAMAAgACAAIAAgACAAEAAQACAAEAAQACAAIAAQABAAEAAgABAAIAAgABAAIAAQABAAIAAgABAAIAAgACAAIAAQABAAIAAgACAAEAAQACAAIAAgACAAIAAgACAAEAAQACAAIAAQABAAIAAgACAAIAAgACAAIAAQABAAEAAQACAAIAAQABAAEAAgACAAEAAQACAAIAAQABAAIAAQACAAIAAQABAAIAAgACAAIAAgABAAEAAgACAAIAAgABAA==",
"AQABAAIAAQABAAEAAgABAAIAAQABAAEAAQACAAIAAQABAAEAAgABAAEAAgACAAIAAgACAAEAAQABAAIAAgACAAEAAQABAAIAAgACAAEAAgACAAEAAQACAAEAAQABAAEAAQABAAEAAQACAAEAAQABAAEAAQABAAEAAQABAAEAAgACAAEAAQABAAIAAgABAAIAAQABAAIAAgABAAIAAgABAAEAAgABAAIAAgACAAIAAgACAAIAAgABAAEAAgACAAEAAQACAAIAAgABAAIAAgABAAEAAQABAAIAAQABAAEAAgABAAEAAgACAAIAAgABAAEAAgABAAEAAgABAAEAAgABAA==",
"AQACAAIAAQABAAIAAQABAAIAAQACAAIAAQACAAIAAgABAAEAAgABAAEAAQABAAIAAgABAAIAAQABAAEAAQABAAEAAQABAAEAAQACAAEAAQABAAIAAgABAAEAAQABAAEAAQACAAEAAQABAAEAAQABAAEAAQABAAEAAAAAAAEAAQABAAEAAQABAAEAAQABAAEAAgACAAEAAgACAAEAAgACAAIAAgABAAEAAQACAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAAAAAAAAAAAAAAAQAAAAEAAQABAAEA/////wAAAAAAAAAAAAAAAAAAAAAAAA==",
],
utterance_idx: 0,
text: "Hey there!",
},
];
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
alignItems: "center",
justifyContent: "flex-start",
paddingTop: 50,
paddingBottom: 50,
},
buttonContainer: {
flexDirection: "row",
justifyContent: "space-around",
width: "100%",
marginBottom: 20,
},
volumeContainer: {
justifyContent: "space-around",
width: "100%",
marginBottom: 20,
},
scrollView: {
flex: 1,
width: "90%",
borderWidth: 1,
borderColor: "#ccc",
},
});

View File

@@ -0,0 +1,44 @@
# Expo Two-Way-Audio Using Flow API
This example showcases the basic functions of the expo-two-way-audio module:
- Capture input raw pcm data
- Play raw pcm data (from a hardcoded base64 string)
- Display volume level for both input and output
- In iOS: show/set microphone mode while recording.
## Requirements
- Node.js
- Expo CLI
- [iOS](https://docs.expo.dev/workflow/ios-simulator/) and/or [Android](https://docs.expo.dev/workflow/android-studio-emulator/) dev environment setup.
## Run the app
In the `examples/basic-usage` directory:
1. Install dependencies:
```sh
npm install
```
2. Run the app:
```sh
# Run iOS
npm run ios
# Run Android
npm run android
```
## Notes
Some audio features of expo-two-way-audio like Acoustic Echo Cancelling, noise reduction or microphone modes (iOS) don't work on simulator. Run the example on a real device to test these features.
```bash
# iOS
npx expo run:ios --device --configuration Release
# Android
npx expo run:android --device --variant release
```

View File

@@ -0,0 +1,47 @@
{
"expo": {
"name": "expo-two-way-audio-example",
"slug": "expo-two-way-audio-example",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "light",
"splash": {
"image": "./assets/splash.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"ios": {
"supportsTablet": false,
"bundleIdentifier": "expo.modules.twowayaudio.example",
"infoPlist": {
"UIBackgroundModes": ["audio"],
"NSMicrophoneUsageDescription": "Allow Speechmatics to access your microphone"
}
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#ffffff"
},
"permissions": ["RECORD_AUDIO", "MODIFY_AUDIO_SETTINGS"],
"package": "expo.modules.twowayaudio.example"
},
"web": {
"favicon": "./assets/favicon.png"
},
"plugins": [
[
"expo-build-properties",
{
"android": {
"compileSdkVersion": 34,
"targetSdkVersion": 31,
"minSdkVersion": 26,
"buildToolsVersion": "34.0.0"
}
}
]
]
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View File

@@ -0,0 +1,6 @@
module.exports = function (api) {
api.cache(true);
return {
presets: ["babel-preset-expo"],
};
};

View File

@@ -0,0 +1,34 @@
// Learn more https://docs.expo.io/guides/customizing-metro
const { getDefaultConfig } = require("expo/metro-config");
const path = require("path");
const config = getDefaultConfig(__dirname);
// npm v7+ will install ../node_modules/react and ../node_modules/react-native because of peerDependencies.
// To prevent the incompatible react-native between ./node_modules/react-native and ../node_modules/react-native,
// excludes the one from the parent folder when bundling.
config.resolver.blockList = [
...Array.from(config.resolver.blockList ?? []),
new RegExp(path.resolve("../..", "node_modules", "react")),
new RegExp(path.resolve("../..", "node_modules", "react-native")),
];
config.resolver.nodeModulesPaths = [
path.resolve(__dirname, "./node_modules"),
path.resolve(__dirname, "../../node_modules"),
];
config.resolver.extraNodeModules = {
"expo-two-way-audio": "../..",
};
config.watchFolders = [path.resolve(__dirname, "../../")];
config.transformer.getTransformOptions = async () => ({
transform: {
experimentalImportSupport: false,
inlineRequires: true,
},
});
module.exports = config;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,30 @@
{
"name": "expo-two-way-audio-example",
"version": "1.0.0",
"main": "expo/AppEntry.js",
"scripts": {
"start": "expo start",
"android": "expo run:android",
"ios": "expo run:ios",
"web": "expo start --web"
},
"dependencies": {
"buffer": "^6.0.3",
"expo": "^52.0.0",
"react": "18.3.1",
"react-native": "0.76.2",
"expo-dev-client": "~5.0.3",
"expo-build-properties": "~0.13.1"
},
"devDependencies": {
"@babel/core": "^7.20.0",
"@types/react": "~18.3.12",
"typescript": "^5.1.3"
},
"private": true,
"expo": {
"autolinking": {
"nativeModulesDir": "../.."
}
}
}

View File

@@ -0,0 +1,11 @@
{
"extends": "expo/tsconfig.base",
"compilerOptions": {
"strict": true,
"module": "ESNext",
"paths": {
"@speechmatics/expo-two-way-audio": ["../../src/index"],
"@speechmatics/expo-two-way-audio/*": ["../../src/*"]
}
}
}

View File

@@ -0,0 +1,20 @@
const createConfigAsync = require("@expo/webpack-config");
const path = require("path");
module.exports = async (env, argv) => {
const config = await createConfigAsync(
{
...env,
babel: {
dangerouslyAddModulePathsToTranspile: ["expo-two-way-audio"],
},
},
argv,
);
config.resolve.modules = [
path.resolve(__dirname, "./node_modules"),
path.resolve(__dirname, "../../node_modules"),
];
return config;
};

View File

@@ -0,0 +1,35 @@
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
# dependencies
node_modules/
# Expo
.expo/
dist/
web-build/
# Native
*.orig.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision
# Metro
.metro-health-check*
# debug
npm-debug.*
yarn-debug.*
yarn-error.*
# macOS
.DS_Store
*.pem
# local env files
.env*.local
# typescript
*.tsbuildinfo

View File

@@ -0,0 +1,270 @@
// EventTarget polyfill is required for the Flow SDK to work in React Native
import "event-target-polyfill";
import { Buffer } from "buffer";
import { Button, StyleSheet, Text, View } from "react-native";
import { Platform } from "react-native";
import {
type MicrophoneDataCallback,
type VolumeLevelCallback,
getMicrophoneModeIOS,
initialize,
playPCMData,
toggleRecording,
useExpoTwoWayAudioEventListener,
useIsRecording,
useMicrophonePermissions,
} from "@speechmatics/expo-two-way-audio";
import {
FlowProvider,
type RecognitionAlternative,
useFlow,
useFlowEventListener,
} from "@speechmatics/flow-client-react";
import { useCallback, useEffect, useState, useSyncExternalStore } from "react";
import { useSharedValue } from "react-native-reanimated";
import { getFlowAPIJwt } from "./auth";
import VolumeDisplay from "./volume-display";
export default function App() {
const [micPermission, requestMicPermission] = useMicrophonePermissions();
console.log(micPermission);
if (!micPermission?.granted) {
return (
<View style={styles.container}>
<Text>Mic permission: {micPermission?.status}</Text>
<Button
title={micPermission?.canAskAgain ? "Request permission" : "Cannot request permissions"}
disabled={!micPermission?.canAskAgain}
onPress={requestMicPermission}
/>
</View>
);
}
return (
<FlowProvider appId="flow-api-example" websocketBinaryType="arraybuffer">
<FlowTest />
</FlowProvider>
);
}
export function FlowTest() {
const [isConnectedToFlow, setIsConnectedToFlow] = useState(false);
const [isConnectingToFlow, setIsConnectingToFlow] = useState(false);
const [audioInitialized, setAudioInitialized] = useState(false);
const inputVolumeLevel = useSharedValue(0.0);
const outputVolumeLevel = useSharedValue(0.0);
const isRecording = useIsRecording();
const { startConversation, endConversation, sendAudio } = useFlow();
// Initialize Expo Two Way Audio
useEffect(() => {
const initializeAudio = async () => {
await initialize();
setAudioInitialized(true);
};
initializeAudio();
}, []);
// Setup a handler for the "message" event from Flow API
useFlowEventListener("message", ({ data }) => {
// For the purpose of this example, just print the content of the transcript
if (data.message === "AddTranscript") {
for (const result of data.results as {
alternatives: Array<RecognitionAlternative>;
}[]) {
for (const alternative of result.alternatives as Array<RecognitionAlternative>) {
console.log(alternative.content);
}
}
}
});
// Setup a handler for the "agentAudio" event from Flow API
useFlowEventListener("agentAudio", (audio) => {
// Even though Int16Array is a more natural representation for PCM16_sle,
// Expo Modules API uses a convertible type for arrays of bytes and it needs Uint8Array in the JS side.
// This is converted to a `Data` type in Swift and to a `kotlin.ByteArray` in Kotlin.
// More info here: https://docs.expo.dev/modules/module-api/#convertibles
// For this reason, the Expo Two Way Audio library requires a Uint8Array argument for the `playPCMData` function.
const byteArray = new Uint8Array(audio.data.buffer);
playPCMData(byteArray);
});
// Setup a handler for the "onMicrophoneData" event from Expo Two Way Audio module
useExpoTwoWayAudioEventListener(
"onMicrophoneData",
useCallback<MicrophoneDataCallback>(
(event) => {
// We send the audio bytes to the Flow API
sendAudio(event.data.buffer);
},
[sendAudio],
),
);
// Setup a handler for the "onInputVolumeLevelData" event from Expo Two Way Audio module.
// This event is triggered when the input volume level changes.
useExpoTwoWayAudioEventListener(
"onInputVolumeLevelData",
useCallback<VolumeLevelCallback>(
(event) => {
// We update the react-native-reanimated shared value for the input volume (microphone).
// This allow us to create animations for the microphone volume based on this shared value.
inputVolumeLevel.value = event.data;
},
[inputVolumeLevel],
),
);
// Setup a handler for the "onOutputVolumeLevelData" event from Expo Two Way Audio module.
// This event is triggered when the output volume level changes.
useExpoTwoWayAudioEventListener(
"onOutputVolumeLevelData",
useCallback<VolumeLevelCallback>(
(event) => {
// We update the react-native-reanimated shared value for the output volume (speaker).
// This allow us to create animations for the speaker volume based on this shared value.
outputVolumeLevel.value = event.data;
},
[outputVolumeLevel],
),
);
// Handle clicks to the 'Connect/Disconnect' button
const handleToggleConnect = useCallback(async () => {
setIsConnectingToFlow(true);
if (isConnectedToFlow) {
endConversation();
setIsConnectedToFlow(false);
} else {
let jwt;
try {
// WARNING: This function is used for the purpose of this example only.
// In a real-world scenario, you should not expose your API key in your client-side code.
// Instead, you should create a server-side endpoint that generates the JWT for you.
jwt = await getFlowAPIJwt(
// EXPO_PUBLIC_SPEECHMATICS_API_KEY could be set in your .env file
process.env.EXPO_PUBLIC_SPEECHMATICS_API_KEY,
);
} catch (error) {
console.error("Failed to get JWT:", error);
setIsConnectingToFlow(false);
return;
}
try {
await startConversation(jwt, {
config: {
template_id: "flow-service-assistant-humphrey",
template_variables: {
timezone: "Europe/London",
},
},
// `audioFormat` is optional. The value below is the default:
audioFormat: {
type: "raw",
encoding: "pcm_s16le", // this can also be set to 'pcm_f32le' for 32-bit Float
sample_rate: 16000,
},
});
setIsConnectedToFlow(true);
} catch (error) {
console.error("Failed to start conversation:", error);
}
}
setIsConnectingToFlow(false);
}, [isConnectedToFlow, startConversation, endConversation]);
// Handle clicks to the 'Mute/Unmute' button
const handleToggleMute = useCallback(() => {
toggleRecording(!isRecording);
}, [isRecording]);
return (
<View style={styles.container}>
<View style={styles.VolumeDisplayContainer}>
<View style={styles.volumeDisplay}>
<VolumeDisplay
volumeLevel={outputVolumeLevel}
color="#7ECFBD"
minSize={130}
maxSize={180}
/>
</View>
<View style={styles.volumeDisplay}>
<VolumeDisplay
volumeLevel={inputVolumeLevel}
color="#4CBBA5"
minSize={70}
maxSize={120}
/>
</View>
</View>
<View>
<Text>
{isConnectedToFlow
? isRecording
? "I'm ready to listen. Try saying something!"
: "Muted. Unmute to start a conversation"
: isConnectingToFlow
? "Connecting..."
: "Disconnected"}
</Text>
</View>
<View style={styles.bottomBar}>
<View style={styles.buttonContainer}>
<Button
title={isConnectedToFlow ? "Disconnect" : "Connect"}
disabled={isConnectingToFlow || !audioInitialized}
onPress={handleToggleConnect}
/>
<Button
title={isRecording ? "Mute" : "Unmute"}
disabled={!isConnectedToFlow || !audioInitialized}
onPress={handleToggleMute}
/>
</View>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
alignItems: "center",
justifyContent: "space-evenly",
padding: 50,
},
buttonContainer: {
flexDirection: "row",
justifyContent: "space-around",
width: "100%",
marginBottom: 20,
},
bottomBar: {
position: "absolute",
bottom: 0,
width: "100%",
padding: 20,
borderTopWidth: 1,
borderTopColor: "lightgray",
},
VolumeDisplayContainer: {
position: "relative",
width: 150,
height: 150,
alignItems: "center",
justifyContent: "center",
},
volumeDisplay: {
position: "absolute",
},
});

View File

@@ -0,0 +1,59 @@
# Expo Two-Way-Audio Using Flow API
This example shows how expo-two-way-audio can be used to communicate with [Speechmatics Flow API](https://www.speechmatics.com/flow)
Flow is a conversational AI API and therefore, we need to send audio data in real time and play back the audio responses that we'll get from the API.
## Requirements
- Node.js
- Expo CLI
- [iOS](https://docs.expo.dev/workflow/ios-simulator/) and/or [Android](https://docs.expo.dev/workflow/android-studio-emulator/) dev environment setup.
- [Speechmatics Flow Client (React)](https://github.com/speechmatics/speechmatics-js-sdk/tree/main/packages/flow-client-react)
- [`event-target-polyfill`](https://www.npmjs.com/package/event-target-polyfill) package, or any other polyfill for the [`EventTarget` class](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget)
A Speechmatics API key is also needed in order to run the example. The key is injected into the app via environment variable. It can be passed by creating a `.env` file in the same directory as the example with the following content:
```
EXPO_PUBLIC_SPEECHMATICS_API_KEY='YOUR-API-KEY-HERE'
```
An API key can be obtained from the [Speechmatics Portal](https://portal.speechmatics.com)
> [!WARNING]
>
> The example uses the API key to request a temporary token (needed for communicating with flow).
> In a real-world scenario, you should not expose your API key in your client-side code.
> Instead, you should create a server-side endpoint that generates the temporary token (JWT) for you.
## Run the app
In the `examples/flow-api` directory:
1. Install dependencies:
```sh
npm install
```
2. Run the app:
```sh
# Run iOS
npm run ios
# Run Android
npm run android
```
## Notes
Some audio features of expo-two-way-audio like Acoustic Echo Cancelling, noise reduction or microphone modes (iOS) don't work on simulator. Run the example on a real device to test these features.
```bash
# iOS
npx expo run:ios --device --configuration Release
# Android
npx expo run:android --device --variant release
```

View File

@@ -0,0 +1,47 @@
{
"expo": {
"name": "Flow Example",
"slug": "expo-two-way-audio-flow-example",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "light",
"splash": {
"image": "./assets/splash.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"ios": {
"supportsTablet": false,
"bundleIdentifier": "expo.modules.twowayaudio.flow.example",
"infoPlist": {
"UIBackgroundModes": ["audio"],
"NSMicrophoneUsageDescription": "Allow Speechmatics to access your microphone"
}
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#ffffff"
},
"permissions": ["RECORD_AUDIO", "MODIFY_AUDIO_SETTINGS"],
"package": "expo.modules.twowayaudio.flow.example"
},
"web": {
"favicon": "./assets/favicon.png"
},
"plugins": [
[
"expo-build-properties",
{
"android": {
"compileSdkVersion": 34,
"targetSdkVersion": 31,
"minSdkVersion": 26,
"buildToolsVersion": "34.0.0"
}
}
]
]
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View File

@@ -0,0 +1,15 @@
import { createSpeechmaticsJWT } from "@speechmatics/auth";
// WARNING: This function is used for the purpose of this example only.
// In a real-world scenario, you should not expose your API key in your client-side code.
// Instead, you should create a server-side endpoint that generates the JWT for you.
export async function getFlowAPIJwt(apiKey: string) {
if (!apiKey) {
throw new Error("API key is required.");
}
return await createSpeechmaticsJWT({
type: "flow",
apiKey,
ttl: 60, // 1 minute
});
}

View File

@@ -0,0 +1,6 @@
module.exports = function (api) {
api.cache(true);
return {
presets: ["babel-preset-expo"],
};
};

View File

@@ -0,0 +1,37 @@
const { wrapWithReanimatedMetroConfig } = require("react-native-reanimated/metro-config");
// Learn more https://docs.expo.io/guides/customizing-metro
const { getDefaultConfig } = require("expo/metro-config");
const path = require("path");
const config = getDefaultConfig(__dirname);
// npm v7+ will install ../node_modules/react and ../node_modules/react-native because of peerDependencies.
// To prevent the incompatible react-native between ./node_modules/react-native and ../node_modules/react-native,
// excludes the one from the parent folder when bundling.
config.resolver.blockList = [
...Array.from(config.resolver.blockList ?? []),
new RegExp(path.resolve("../..", "node_modules", "react")),
new RegExp(path.resolve("../..", "node_modules", "react-native")),
];
config.resolver.nodeModulesPaths = [
path.resolve(__dirname, "./node_modules"),
path.resolve(__dirname, "../../node_modules"),
];
config.resolver.extraNodeModules = {
"expo-two-way-audio": "../..",
};
config.watchFolders = [path.resolve(__dirname, "../../")];
config.transformer.getTransformOptions = async () => ({
transform: {
experimentalImportSupport: false,
inlineRequires: true,
},
});
module.exports = wrapWithReanimatedMetroConfig(config);
config;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,34 @@
{
"name": "expo-two-way-audio-flow-example",
"version": "1.0.0",
"main": "expo/AppEntry.js",
"scripts": {
"start": "expo start",
"android": "expo run:android",
"ios": "expo run:ios",
"web": "expo start --web"
},
"dependencies": {
"@speechmatics/auth": "^0.0.1",
"@speechmatics/flow-client-react": "^0.2.0",
"buffer": "^6.0.3",
"event-target-polyfill": "^0.0.4",
"expo": "^52.0.25",
"expo-build-properties": "~0.13.2",
"expo-dev-client": "~5.0.9",
"react": "18.3.1",
"react-native": "0.76.6",
"react-native-reanimated": "~3.16.1"
},
"devDependencies": {
"@babel/core": "^7.20.0",
"@types/react": "~18.3.12",
"typescript": "^5.1.3"
},
"private": true,
"expo": {
"autolinking": {
"nativeModulesDir": "../.."
}
}
}

View File

@@ -0,0 +1,11 @@
{
"extends": "expo/tsconfig.base",
"compilerOptions": {
"strict": true,
"module": "ESNext",
"paths": {
"@speechmatics/expo-two-way-audio": ["../../src/index"],
"@speechmatics/expo-two-way-audio/*": ["../../src/*"]
}
}
}

View File

@@ -0,0 +1,52 @@
import { StyleSheet, View } from "react-native";
import Animated, {
Easing,
interpolate,
useAnimatedStyle,
withTiming,
type SharedValue,
} from "react-native-reanimated";
export default function VolumeDisplay({
volumeLevel,
color,
minSize,
maxSize,
}: {
volumeLevel: SharedValue<number>;
color: string;
minSize: number;
maxSize: number;
}) {
const animatedStyle = useAnimatedStyle(() => {
const size = interpolate(volumeLevel.value, [0, 1], [minSize, maxSize]);
return {
width: withTiming(size, {
duration: 100,
easing: Easing.linear,
}),
height: withTiming(size, {
duration: 100,
easing: Easing.linear,
}),
borderRadius: withTiming(size / 2, {
duration: 100,
easing: Easing.linear,
}),
};
});
return (
<View style={styles.container}>
<Animated.View style={[{ backgroundColor: color }, animatedStyle]} />
</View>
);
}
const styles = StyleSheet.create({
container: {
justifyContent: "center",
alignItems: "center",
},
});

View File

@@ -0,0 +1,20 @@
const createConfigAsync = require("@expo/webpack-config");
const path = require("path");
module.exports = async (env, argv) => {
const config = await createConfigAsync(
{
...env,
babel: {
dangerouslyAddModulePathsToTranspile: ["expo-two-way-audio"],
},
},
argv,
);
config.resolve.modules = [
path.resolve(__dirname, "./node_modules"),
path.resolve(__dirname, "../../node_modules"),
];
return config;
};

View File

@@ -0,0 +1,9 @@
{
"platforms": ["ios", "android"],
"ios": {
"modules": ["ExpoTwoWayAudioModule"]
},
"android": {
"modules": ["expo.modules.twowayaudio.ExpoTwoWayAudioModule"]
}
}

View File

@@ -0,0 +1,374 @@
import AVFoundation
import Foundation
class AudioEngine {
private var avAudioEngine = AVAudioEngine()
private var speechPlayer = AVAudioPlayerNode()
private var engineConfigChangeObserver: Any?
private var sessionInterruptionObserver: Any?
private var mediaServicesResetObserver: Any?
public private(set) var voiceIOFormat: AVAudioFormat
public private(set) var isRecording = false
public var onMicDataCallback: ((Data) -> Void)?
public var onInputVolumeCallback: ((Float) -> Void)?
public var onOutputVolumeCallback: ((Float) -> Void)?
public var onAudioInterruptionCallback: ((String) -> Void)?
private var inputLevelTimer: Timer?
private var outputLevelTimer: Timer?
private var inputBuffer = [Float](repeating: 0, count: 2048)
private var outputBuffer = [Float](repeating: 0, count: 2048)
private var inputBufferIndex = 0
private var outputBufferIndex = 0
private var hasFirstInputBeenDiscarded = false
private var discardRecording = false
private var discardFirstInputMillis = 2000
enum AudioEngineError: Error {
case audioFormatError
}
init() throws {
avAudioEngine.attach(speechPlayer)
guard let format = AVAudioFormat(standardFormatWithSampleRate: 16000, channels: 1) else {
throw AudioEngineError.audioFormatError
}
voiceIOFormat = format
print("Voice IO format: \(String(describing: voiceIOFormat))")
engineConfigChangeObserver = NotificationCenter.default.addObserver(
forName: .AVAudioEngineConfigurationChange,
object: avAudioEngine,
queue: .main) { [weak self] _ in
self?.checkEngineIsRunning()
}
sessionInterruptionObserver = NotificationCenter.default.addObserver(
forName: AVAudioSession.interruptionNotification,
object: AVAudioSession.sharedInstance(),
queue: .main) { [weak self] notification in
self?.handleAudioSessionInterruption(notification)
}
mediaServicesResetObserver = NotificationCenter.default.addObserver(
forName: AVAudioSession.mediaServicesWereResetNotification,
object: AVAudioSession.sharedInstance(),
queue: .main) { [weak self] _ in
self?.handleMediaServicesWereReset()
}
self.setupAudioSession()
self.setup()
self.start()
}
deinit {
if let observer = engineConfigChangeObserver {
NotificationCenter.default.removeObserver(observer)
}
if let observer = sessionInterruptionObserver {
NotificationCenter.default.removeObserver(observer)
}
if let observer = mediaServicesResetObserver {
NotificationCenter.default.removeObserver(observer)
}
}
func setupAudioSession() {
let session = AVAudioSession.sharedInstance()
do {
try session.setCategory(.playAndRecord, mode: .voiceChat, options: [.defaultToSpeaker, .allowBluetooth, .allowBluetoothA2DP])
} catch {
print("Could not set the audio category: \(error.localizedDescription)")
}
do {
try session.setPreferredSampleRate(voiceIOFormat.sampleRate)
} catch {
print("Could not set the preferred sample rate: \(error.localizedDescription)")
}
do {
try session.setActive(true)
} catch {
print("Could not set the audio session as active")
}
}
func setup() {
let input = avAudioEngine.inputNode
do {
try input.setVoiceProcessingEnabled(true)
} catch {
print("Could not enable voice processing \(error)")
return
}
avAudioEngine.inputNode.isVoiceProcessingInputMuted = !isRecording
let output = avAudioEngine.outputNode
let mainMixer = avAudioEngine.mainMixerNode
avAudioEngine.connect(speechPlayer, to: mainMixer, format: voiceIOFormat)
avAudioEngine.connect(mainMixer, to: output, format: voiceIOFormat)
input.installTap(onBus: 0, bufferSize: 2048, format: voiceIOFormat) { [weak self] buffer, when in
// We don't do any input processing (no volume calculation or passing mic data to the callback) if discardRecording == true
// See comment in the playPCMData function
if self?.isRecording == true && self?.discardRecording == false {
self?.processMicrophoneBuffer(buffer)
self?.updateInputVolume()
}
}
mainMixer.installTap(onBus: 0, bufferSize: 2048, format: voiceIOFormat) { [weak self] buffer, when in
self?.processOutputBuffer(buffer)
self?.updateOutputVolume()
}
avAudioEngine.prepare()
}
func processMicrophoneBuffer(_ buffer: AVAudioPCMBuffer) {
guard let channelData = buffer.floatChannelData?[0] else {
print("Error: Could not access channel data")
return
}
let frameCount = Int(buffer.frameLength)
var int16Samples = [Int16](repeating: 0, count: frameCount)
// Convert float samples to Int16 and update input buffer for volume calculation
for i in 0..<frameCount {
let floatSample = max(-1.0, min(1.0, channelData[i]))
int16Samples[i] = Int16(floatSample * Float(Int16.max))
inputBuffer[inputBufferIndex] = floatSample
inputBufferIndex = (inputBufferIndex + 1) % inputBuffer.count
}
// Create Data object from Int16 samples
let data = Data(bytes: int16Samples, count: frameCount * MemoryLayout<Int16>.size)
// Send the data to the callback
onMicDataCallback?(data)
}
func processOutputBuffer(_ buffer: AVAudioPCMBuffer) {
guard let channelData = buffer.floatChannelData?[0] else {
print("Error: Could not access channel data")
return
}
let frameCount = Int(buffer.frameLength)
// Update output buffer for volume calculation
for i in 0..<frameCount {
let floatSample = max(-1.0, min(1.0, channelData[i]))
outputBuffer[outputBufferIndex] = floatSample
outputBufferIndex = (outputBufferIndex + 1) % outputBuffer.count
}
}
func start() {
do {
try avAudioEngine.start()
} catch {
print("Could not start audio engine: \(error)")
}
}
func playPCMData(_ pcmData: Data) {
// Looks like we don't get a proper AEC for the very first chunks of audio that we play.
// To work around this, we will discard microphone input for the first few milliseconds.
// This will give the AEC time to adapt to the playback audio.
// We achieve this by setting discardRecording to true for a short time (this doesn't actually mute the input). It's just not processed in the tap.
// Audio that is not processed by the input tap is not sent to the callback and therefore not sent to the server.
if !hasFirstInputBeenDiscarded {
self.hasFirstInputBeenDiscarded = true
self.discardRecording = true
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(discardFirstInputMillis)) {
self.discardRecording = false
}
}
guard let buffer = createBuffer(from: pcmData) else {
print("Failed to create audio buffer")
return
}
speechPlayer.scheduleBuffer(buffer)
if !speechPlayer.isPlaying {
speechPlayer.play()
}
}
private func createBuffer(from data: Data) -> AVAudioPCMBuffer? {
let frameCount = UInt32(data.count) / 2 // 16-bit input = 2 bytes per frame
let format = AVAudioFormat(commonFormat: .pcmFormatFloat32,
sampleRate: 16000,
channels: 1,
interleaved: false)!
guard let buffer = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: frameCount) else {
return nil
}
buffer.frameLength = frameCount
data.withUnsafeBytes { (rawBufferPointer: UnsafeRawBufferPointer) in
if let sourcePtr = rawBufferPointer.baseAddress?.assumingMemoryBound(to: Int16.self),
let destPtr = buffer.floatChannelData?[0] {
for i in 0..<Int(frameCount) {
destPtr[i] = Float(sourcePtr[i]) / Float(Int16.max)
}
}
}
return buffer
}
func bypassVoiceProcessing(_ bypass: Bool) {
let input = avAudioEngine.inputNode
input.isVoiceProcessingBypassed = bypass
}
func toggleRecording(_ val: Bool) -> Bool {
isRecording = val
if !isRecording {
avAudioEngine.inputNode.isVoiceProcessingInputMuted = true
// Reset input buffer, so that volume levels report 0
inputBuffer = [Float](repeating: 0, count: 2048)
updateInputVolume()
} else {
avAudioEngine.inputNode.isVoiceProcessingInputMuted = false
}
print("Recording \(isRecording ? "started" : "stopped")")
return isRecording
}
func stopRecordingAndPlayer(){
do {
try AVAudioSession.sharedInstance().setActive(false)
} catch {
print("Could not set the audio session to inactive: \(error)")
}
toggleRecording(false)
speechPlayer.stop()
updateOutputVolume()
}
func resumeRecordingAndPlayer(){
do {
try AVAudioSession.sharedInstance().setActive(true)
} catch {
print("Could not set the audio session to active: \(error)")
}
self.checkEngineIsRunning()
isRecording = toggleRecording(true)
speechPlayer.play()
}
func tearDown() {
stopRecordingAndPlayer()
avAudioEngine.stop()
}
var isPlaying: Bool {
return speechPlayer.isPlaying
}
func stopPlayback() {
speechPlayer.stop()
// Clear any scheduled buffers
outputBuffer = [Float](repeating: 0, count: 2048)
updateOutputVolume()
print("Playback stopped")
}
func pausePlayback() {
speechPlayer.pause()
print("Playback paused")
}
func resumePlayback() {
if !speechPlayer.isPlaying {
speechPlayer.play()
print("Playback resumed")
}
}
private func checkEngineIsRunning() {
if !avAudioEngine.isRunning {
start()
}
}
private func handleAudioSessionInterruption(_ notification: Notification) {
guard let userInfo = notification.userInfo,
let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
let type = AVAudioSession.InterruptionType(rawValue: typeValue) else { return }
switch type {
case .began:
self.stopRecordingAndPlayer()
onAudioInterruptionCallback?("began")
case .ended:
if let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt {
let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
if options.contains(.shouldResume) {
// Interruption ended. Resume playback.
self.resumeRecordingAndPlayer()
onAudioInterruptionCallback?("ended")
} else {
// Interruption ends. Don't resume playback.
onAudioInterruptionCallback?("blocked")
}
}
@unknown default:
fatalError("Unknown type: \(type)")
}
}
private func handleMediaServicesWereReset() {
self.avAudioEngine.stop()
self.setup()
self.start()
}
private func updateInputVolume() {
let volume = calculateRMSLevel(from: inputBuffer)
onInputVolumeCallback?(volume)
}
private func updateOutputVolume() {
let volume = calculateRMSLevel(from: outputBuffer)
onOutputVolumeCallback?(volume)
}
private func calculateRMSLevel(from buffer: [Float]) -> Float {
let epsilon: Float = 1e-5 // To avoid log(0)
let rmsValue = sqrt(buffer.reduce(0) { $0 + $1 * $1 } / Float(buffer.count))
// Convert to decibels
let dbValue = 20 * log10(max(rmsValue, epsilon))
// Normalize decibel value to 0-1 range
// Assuming minimum audible is -60dB and maximum is 0dB
let minDb: Float = -80.0
let normalizedValue = max(0.0, min(1.0, (dbValue - minDb) / abs(minDb)))
// Optional: Apply exponential factor to push smaller values down
let expFactor: Float = 2.0 // Adjust this value to change the curve
let adjustedValue = pow(normalizedValue, expFactor)
return adjustedValue
}
}

View File

@@ -0,0 +1,27 @@
require 'json'
package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
Pod::Spec.new do |s|
s.name = 'ExpoTwoWayAudio'
s.version = package['version']
s.summary = package['description']
s.description = package['description']
s.license = package['license']
s.author = package['author']
s.homepage = package['homepage']
s.platforms = { :ios => '13.4', :tvos => '13.4' }
s.swift_version = '5.4'
s.source = { git: 'https://github.com/speechmatics/expo-two-way-audio' }
s.static_framework = true
s.dependency 'ExpoModulesCore'
# Swift/Objective-C compatibility
s.pod_target_xcconfig = {
'DEFINES_MODULE' => 'YES',
'SWIFT_COMPILATION_MODE' => 'wholemodule'
}
s.source_files = "**/*.{h,m,swift}"
end

View File

@@ -0,0 +1,211 @@
import ExpoModulesCore
let ON_MIC_DATA_EVENT_NAME = "onMicrophoneData"
let ON_INPUT_VOLUME_LEVEL_EVENT_NAME = "onInputVolumeLevelData"
let ON_OUTPUT_VOLUME_LEVEL_EVENT_NAME = "onOutputVolumeLevelData"
let ON_RECORDING_CHANGE_EVENT_NAME = "onRecordingChange"
let ON_AUDIO_INTERRUPTION_EVENT_NAME = "onAudioInterruption"
public class ExpoTwoWayAudioModule: Module {
private var audioEngine: AudioEngine?
public func definition() -> ModuleDefinition {
Name("ExpoTwoWayAudio")
OnCreate {
let permissionsManager = self.appContext?.permissions
EXPermissionsMethodsDelegate.register(
[
MicrophonePermissionRequester()
],
withPermissionsManager: permissionsManager
)
}
AsyncFunction("initialize") { () -> Bool in
do {
if self.audioEngine != nil {
return true
}
self.audioEngine = try AudioEngine()
self.setupMicrophoneCallback()
self.setupInputAudioLevelCallback()
self.setupOutputAudioLevelCallback()
self.setupAudioInterruptionCallback()
return true
} catch {
print("Failed to initialize AudioEngine: \(error)")
return false
}
}
Function("isRecording") { () -> Bool in
guard let audioEngine = self.audioEngine else {
print("AudioEngine not initialized")
return false
}
return audioEngine.isRecording
}
Function("toggleRecording") { (val: Bool) -> Bool in
guard let audioEngine = self.audioEngine else {
print("AudioEngine not initialized")
return false
}
let isRecording = audioEngine.toggleRecording(val)
self.sendEvent(
ON_RECORDING_CHANGE_EVENT_NAME,
[
"data": isRecording
])
return isRecording
}
Function("getMicrophoneModeIOS") { () -> String in
if #available(iOS 15.0, *) {
let mode = AVCaptureDevice.preferredMicrophoneMode.rawValue
var micMode = ""
switch mode {
case 1:
micMode = MicrophoneMode.wideSpectrum.rawValue
case 2:
micMode = MicrophoneMode.voiceIsolation.rawValue
default:
micMode = MicrophoneMode.standard.rawValue
}
return micMode
}
print("Please update your ios")
return ""
}
Function("setMicrophoneModeIOS") {
if #available(iOS 15, *) {
if AVCaptureDevice.preferredMicrophoneMode != .voiceIsolation {
AVCaptureDevice.showSystemUserInterface(.microphoneModes)
}
} else {
print("This code only runs on iOS 14 and lower")
}
}
Function("tearDown") {
self.audioEngine?.tearDown()
self.audioEngine = nil
}
Function("restart") {
self.audioEngine?.resumeRecordingAndPlayer()
self.sendEvent(
ON_RECORDING_CHANGE_EVENT_NAME,
[
"data": audioEngine?.isRecording
])
}
Function("playPCMData") { (pcmData: Data) in
self.audioEngine?.playPCMData(pcmData)
}
Function("bypassVoiceProcessing") { (bypass: Bool) in
self.audioEngine?.bypassVoiceProcessing(bypass)
}
Function("isPlaying") { () -> Bool in
return self.audioEngine?.isPlaying ?? false
}
Function("stopPlayback") {
self.audioEngine?.stopPlayback()
}
Function("pausePlayback") {
self.audioEngine?.pausePlayback()
}
Function("resumePlayback") {
self.audioEngine?.resumePlayback()
}
AsyncFunction("getMicrophonePermissionsAsync") { (promise: Promise) in
EXPermissionsMethodsDelegate.getPermissionWithPermissionsManager(
self.appContext?.permissions,
withRequester: MicrophonePermissionRequester.self,
resolve: promise.resolver,
reject: promise.legacyRejecter
)
}
AsyncFunction("requestMicrophonePermissionsAsync") { (promise: Promise) in
EXPermissionsMethodsDelegate.askForPermission(
withPermissionsManager: self.appContext?.permissions,
withRequester: MicrophonePermissionRequester.self,
resolve: promise.resolver,
reject: promise.legacyRejecter
)
}
// Define the events that can be emitted
Events([
ON_MIC_DATA_EVENT_NAME,
ON_INPUT_VOLUME_LEVEL_EVENT_NAME,
ON_OUTPUT_VOLUME_LEVEL_EVENT_NAME,
ON_RECORDING_CHANGE_EVENT_NAME,
ON_AUDIO_INTERRUPTION_EVENT_NAME,
])
}
private func setupMicrophoneCallback() {
audioEngine?.onMicDataCallback = { [weak self] data in
self?.sendEvent(
ON_MIC_DATA_EVENT_NAME,
[
"data": data
])
}
}
private func setupInputAudioLevelCallback() {
audioEngine?.onInputVolumeCallback = { [weak self] level in
self?.sendEvent(
ON_INPUT_VOLUME_LEVEL_EVENT_NAME,
[
"data": level
])
}
}
private func setupOutputAudioLevelCallback() {
audioEngine?.onOutputVolumeCallback = { [weak self] level in
self?.sendEvent(
ON_OUTPUT_VOLUME_LEVEL_EVENT_NAME,
[
"data": level
])
}
}
private func setupAudioInterruptionCallback() {
audioEngine?.onAudioInterruptionCallback = { [weak self] data in
self?.sendEvent(
ON_AUDIO_INTERRUPTION_EVENT_NAME,
[
"data": data
])
self?.sendEvent(
ON_RECORDING_CHANGE_EVENT_NAME,
[
"data": self?.audioEngine?.isRecording
])
}
}
enum MicrophoneMode: String, Enumerable {
case standard
case voiceIsolation
case wideSpectrum
}
}

View File

@@ -0,0 +1,52 @@
import ExpoModulesCore
import AVFoundation
let microphoneKey = "NSMicrophoneUsageDescription"
class MicrophonePermissionRequester: NSObject, EXPermissionsRequester {
let mediaType: AVMediaType = .audio
static func permissionType() -> String {
"microphone"
}
func getPermissions() -> [AnyHashable: Any] {
let systemStatus = AVAudioSession.sharedInstance().recordPermission
var status: EXPermissionStatus
guard (Bundle.main.infoDictionary?["NSMicrophoneUsageDescription"]) != nil else {
EXFatal(EXErrorWithMessage("""
This app is missing NSMicrophoneUsageDescription, so audio services will fail.
Add one of these keys to your bundle's Info.plist.
"""))
return ["status": EXPermissionStatusDenied]
}
switch systemStatus {
case .granted:
status = EXPermissionStatusGranted
case .denied:
status = EXPermissionStatusDenied
case .undetermined:
status = EXPermissionStatusUndetermined
@unknown default:
status = EXPermissionStatusUndetermined
}
return [
"status": status.rawValue
]
}
func requestPermissions(resolver resolve: @escaping EXPromiseResolveBlock, rejecter reject: EXPromiseRejectBlock) {
if #available(iOS 17.0, *) {
AVAudioApplication.requestRecordPermission { _ in
resolve(self.getPermissions())
}
} else {
AVAudioSession.sharedInstance().requestRecordPermission { _ in
resolve(self.getPermissions())
}
}
}
}

17569
packages/expo-two-way-audio/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,43 @@
{
"name": "@getpaseo/expo-two-way-audio",
"version": "0.1.4",
"description": "Native module for two way audio streaming",
"main": "build/index.js",
"types": "build/index.d.ts",
"scripts": {
"build": "expo-module build",
"clean": "expo-module clean",
"lint": "expo-module lint",
"test": "expo-module test",
"format": "biome check .",
"format:fix": "biome check --write .",
"typecheck": "tsc",
"expo-module": "expo-module",
"open:ios": "xed examples/basic-usage/ios",
"open:android": "open -a \"Android Studio\" examples/basic-usage/android"
},
"keywords": [
"react-native",
"expo",
"expo-two-way-audio",
"ExpoTwoWayAudio"
],
"repository": "https://github.com/boudra/expo-two-way-audio",
"bugs": {
"url": "https://github.com/speechmatics/expo-two-way-audio/issues"
},
"author": "Speechmatics <devrel@speechmatics.com> (https://github.com/speechmatics)",
"license": "MIT",
"homepage": "https://github.com/boudra/expo-two-way-audio",
"devDependencies": {
"@biomejs/biome": "1.9.4",
"@types/react": "^18.0.25",
"expo-module-scripts": "^3.5.2",
"expo-modules-core": "^2.0.4"
},
"peerDependencies": {
"expo": "*",
"react": "*",
"react-native": "*"
}
}

View File

@@ -0,0 +1,5 @@
import { requireNativeModule } from "expo-modules-core";
// It loads the native module object from the JSI or falls back to
// the bridge module (from NativeModulesProxy) if the remote debugger is on.
export default requireNativeModule("ExpoTwoWayAudio");

View File

@@ -0,0 +1,62 @@
import { type PermissionResponse, createPermissionHook } from "expo-modules-core";
import ExpoTwoWayAudioModule from "./ExpoTwoWayAudioModule";
export async function initialize() {
return await ExpoTwoWayAudioModule.initialize();
}
export function playPCMData(audioData: Uint8Array) {
return ExpoTwoWayAudioModule.playPCMData(audioData);
}
export function bypassVoiceProcessing(bypass: boolean) {
return ExpoTwoWayAudioModule.bypassVoiceProcessing(bypass);
}
export function toggleRecording(val: boolean): boolean {
return ExpoTwoWayAudioModule.toggleRecording(val);
}
export function isRecording(): boolean {
return ExpoTwoWayAudioModule.isRecording();
}
export function tearDown() {
return ExpoTwoWayAudioModule.tearDown();
}
export function restart() {
return ExpoTwoWayAudioModule.restart();
}
export async function getMicrophonePermissionsAsync(): Promise<PermissionResponse> {
return ExpoTwoWayAudioModule.getMicrophonePermissionsAsync();
}
export async function requestMicrophonePermissionsAsync(): Promise<PermissionResponse> {
return ExpoTwoWayAudioModule.requestMicrophonePermissionsAsync();
}
export function getMicrophoneModeIOS() {
return ExpoTwoWayAudioModule.getMicrophoneModeIOS();
}
export function setMicrophoneModeIOS() {
return ExpoTwoWayAudioModule.setMicrophoneModeIOS();
}
export function isPlaying(): boolean {
return ExpoTwoWayAudioModule.isPlaying();
}
export function stopPlayback() {
return ExpoTwoWayAudioModule.stopPlayback();
}
export function pausePlayback() {
return ExpoTwoWayAudioModule.pausePlayback();
}
export function resumePlayback() {
return ExpoTwoWayAudioModule.resumePlayback();
}

View File

@@ -0,0 +1,38 @@
import ExpoTwoWayAudioModule from "./ExpoTwoWayAudioModule";
export type MicrophoneDataEvent = {
data: Uint8Array;
};
export type VolumeLevelEvent = {
data: number;
};
export type RecordingChangeEvent = {
data: boolean;
};
export type AudioInterruptionEvent = {
data: string;
};
export interface ExpoTwoWayAudioEventMap {
onMicrophoneData: MicrophoneDataEvent;
onInputVolumeLevelData: VolumeLevelEvent;
onOutputVolumeLevelData: VolumeLevelEvent;
onRecordingChange: RecordingChangeEvent;
onAudioInterruption: AudioInterruptionEvent;
}
// These are useful for defining `useCallback` types inline
export type MicrophoneDataCallback = (event: MicrophoneDataEvent) => void;
export type VolumeLevelCallback = (event: VolumeLevelEvent) => void;
export type RecordingChangeCallback = (event: RecordingChangeEvent) => void;
export type AudioInterruptionCallback = (event: AudioInterruptionEvent) => void;
export function addExpoTwoWayAudioEventListener<K extends keyof ExpoTwoWayAudioEventMap>(
eventName: K,
handler: (ev: ExpoTwoWayAudioEventMap[K]) => void,
) {
return ExpoTwoWayAudioModule.addListener(eventName, handler);
}

View File

@@ -0,0 +1,34 @@
import { createPermissionHook } from "expo-modules-core";
import { useEffect, useSyncExternalStore } from "react";
import {
getMicrophonePermissionsAsync,
isRecording,
requestMicrophonePermissionsAsync,
} from "./core";
import { ExpoTwoWayAudioEventMap, addExpoTwoWayAudioEventListener } from "./events";
export const useMicrophonePermissions = createPermissionHook({
getMethod: getMicrophonePermissionsAsync,
requestMethod: requestMicrophonePermissionsAsync,
});
export function useIsRecording() {
const subscribe = (cb: () => void) => {
const sub = addExpoTwoWayAudioEventListener("onRecordingChange", cb);
return () => sub.remove();
};
const getSnapshot = () => isRecording();
const getServerSnapshot = () => false;
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
}
export function useExpoTwoWayAudioEventListener<K extends keyof ExpoTwoWayAudioEventMap>(
eventName: K,
listener: (ev: ExpoTwoWayAudioEventMap[K]) => void,
) {
useEffect(() => {
const sub = addExpoTwoWayAudioEventListener(eventName, listener);
return () => sub.remove();
}, [listener]);
}

View File

@@ -0,0 +1,3 @@
export * from "./core";
export * from "./events";
export * from "./hooks";

View File

@@ -0,0 +1,9 @@
// @generated by expo-module-scripts
{
"extends": "expo-module-scripts/tsconfig.base",
"compilerOptions": {
"outDir": "./build"
},
"include": ["./src"],
"exclude": ["**/__mocks__/*", "**/__tests__/*"]
}

View File

@@ -2130,7 +2130,12 @@ export class Session {
agentId?: string,
requestId?: string
): Promise<void> {
const startedAt = Date.now()
try {
this.sessionLogger.info(
{ enabled, requestedAgentId: agentId ?? null, requestId: requestId ?? null },
'set_voice_mode started'
)
if (enabled) {
const unavailable = this.resolveVoiceFeatureUnavailableContext('voice_mode')
if (unavailable) {
@@ -2144,19 +2149,44 @@ export class Session {
this.voiceModeAgentId &&
this.voiceModeAgentId !== normalizedAgentId
) {
this.sessionLogger.info(
{
previousAgentId: this.voiceModeAgentId,
nextAgentId: normalizedAgentId,
elapsedMs: Date.now() - startedAt,
},
'set_voice_mode disabling previous active voice agent'
)
await this.disableVoiceModeForActiveAgent(true)
}
if (!this.isVoiceMode || this.voiceModeAgentId !== normalizedAgentId) {
this.sessionLogger.info(
{ agentId: normalizedAgentId, elapsedMs: Date.now() - startedAt },
'set_voice_mode enabling voice for agent'
)
const refreshedAgentId = await this.enableVoiceModeForAgent(normalizedAgentId)
this.voiceModeAgentId = refreshedAgentId
this.sessionLogger.info(
{ agentId: refreshedAgentId, elapsedMs: Date.now() - startedAt },
'set_voice_mode agent enable complete'
)
}
this.sessionLogger.info(
{ agentId: this.voiceModeAgentId, elapsedMs: Date.now() - startedAt },
'set_voice_mode starting voice turn controller'
)
await this.startVoiceTurnController()
this.sessionLogger.info(
{ agentId: this.voiceModeAgentId, elapsedMs: Date.now() - startedAt },
'set_voice_mode voice turn controller started'
)
this.isVoiceMode = true
this.sessionLogger.info(
{
agentId: this.voiceModeAgentId,
elapsedMs: Date.now() - startedAt,
},
'Voice mode enabled for existing agent'
)
@@ -2175,9 +2205,13 @@ export class Session {
return
}
this.sessionLogger.info(
{ agentId: this.voiceModeAgentId, elapsedMs: Date.now() - startedAt },
'set_voice_mode disabling active voice mode'
)
await this.disableVoiceModeForActiveAgent(true)
this.isVoiceMode = false
this.sessionLogger.info('Voice mode disabled')
this.sessionLogger.info({ elapsedMs: Date.now() - startedAt }, 'Voice mode disabled')
if (requestId) {
this.emit({
type: 'set_voice_mode_response',
@@ -2198,6 +2232,7 @@ export class Session {
err: error,
enabled,
requestedAgentId: agentId ?? null,
elapsedMs: Date.now() - startedAt,
},
'set_voice_mode failed'
)
@@ -2256,14 +2291,25 @@ export class Session {
}
private async enableVoiceModeForAgent(agentId: string): Promise<string> {
const startedAt = Date.now()
const ensureVoiceSocket = this.ensureVoiceMcpSocketForAgent
if (!ensureVoiceSocket) {
throw new Error('Voice MCP socket bridge is not configured')
}
this.sessionLogger.info({ agentId }, 'enableVoiceModeForAgent.ensureAgentLoaded.start')
const existing = await this.ensureAgentLoaded(agentId)
this.sessionLogger.info(
{ agentId, elapsedMs: Date.now() - startedAt },
'enableVoiceModeForAgent.ensureAgentLoaded.done'
)
this.sessionLogger.info({ agentId }, 'enableVoiceModeForAgent.ensureVoiceSocket.start')
const socketPath = await ensureVoiceSocket(agentId)
this.sessionLogger.info(
{ agentId, socketPath, elapsedMs: Date.now() - startedAt },
'enableVoiceModeForAgent.ensureVoiceSocket.done'
)
this.registerVoiceBridgeForAgent(agentId)
const baseConfig: VoiceModeBaseConfig = {
@@ -2277,7 +2323,15 @@ export class Session {
}
try {
this.sessionLogger.info(
{ agentId, elapsedMs: Date.now() - startedAt },
'enableVoiceModeForAgent.reloadAgentSession.start'
)
const refreshed = await this.agentManager.reloadAgentSession(agentId, refreshOverrides)
this.sessionLogger.info(
{ agentId, refreshedAgentId: refreshed.id, elapsedMs: Date.now() - startedAt },
'enableVoiceModeForAgent.reloadAgentSession.done'
)
return refreshed.id
} catch (error) {
this.unregisterVoiceSpeakHandler?.(agentId)
@@ -2331,6 +2385,7 @@ export class Session {
private async startVoiceTurnController(): Promise<void> {
if (this.voiceTurnController) {
this.sessionLogger.info('startVoiceTurnController skipped: already running')
return
}
@@ -2339,6 +2394,11 @@ export class Session {
throw new Error('Voice turn detection is not configured')
}
this.sessionLogger.info(
{ providerId: turnDetection.id },
'startVoiceTurnController creating controller'
)
const controller = createVoiceTurnController({
logger: this.sessionLogger.child({ component: 'voice-turn-controller' }),
turnDetection,
@@ -2381,8 +2441,10 @@ export class Session {
},
})
this.sessionLogger.info('startVoiceTurnController connecting controller')
await controller.start()
this.voiceTurnController = controller
this.sessionLogger.info('startVoiceTurnController connected')
}
private async stopVoiceTurnController(): Promise<void> {
@@ -6368,12 +6430,9 @@ export class Session {
}
private async processCompletedAudio(audio: Buffer, format: string): Promise<void> {
const shouldBuffer =
this.processingPhase === 'transcribing' && this.pendingAudioSegments.length === 0
if (shouldBuffer) {
if (this.processingPhase === 'transcribing') {
this.sessionLogger.debug(
{ phase: this.processingPhase },
{ phase: this.processingPhase, segmentCount: this.pendingAudioSegments.length + 1 },
`Buffering audio segment (phase: ${this.processingPhase})`
)
this.pendingAudioSegments.push({
@@ -6408,6 +6467,26 @@ export class Session {
await this.processAudio(audio, format)
}
private async flushPendingAudioSegments(reason: string): Promise<void> {
if (this.processingPhase === 'transcribing' || this.pendingAudioSegments.length === 0) {
return
}
const pendingSegments = [...this.pendingAudioSegments]
this.pendingAudioSegments = []
this.clearBufferTimeout()
this.sessionLogger.debug(
{ reason, segmentCount: pendingSegments.length },
`Flushing ${pendingSegments.length} buffered audio segment(s)`
)
const combinedAudio = Buffer.concat(pendingSegments.map((segment) => segment.audio))
const combinedFormat = pendingSegments[pendingSegments.length - 1]!.format
await this.processAudio(combinedAudio, combinedFormat)
}
/**
* Process audio through STT and then LLM
*/
@@ -6456,6 +6535,7 @@ export class Session {
} catch (error: any) {
this.setPhase('idle')
this.clearSpeechInProgress('transcription error')
await this.flushPendingAudioSegments('transcription error')
this.emit({
type: 'activity_log',
payload: {
@@ -6495,6 +6575,7 @@ export class Session {
this.sessionLogger.debug('Empty transcription (false positive), not aborting')
this.setPhase('idle')
this.clearSpeechInProgress('empty transcription')
await this.flushPendingAudioSegments('empty transcription')
return
}
@@ -6539,6 +6620,7 @@ export class Session {
{ requestId: result.requestId },
'Skipping voice agent processing because voice mode is disabled'
)
await this.flushPendingAudioSegments('voice mode disabled')
return
}
@@ -6548,12 +6630,14 @@ export class Session {
{ requestId: result.requestId },
'Skipping voice agent processing because no agent is currently voice-enabled'
)
await this.flushPendingAudioSegments('no active voice agent')
return
}
// Route voice utterances through the same send path as regular text input:
// interrupt-if-running, record message, then start a new stream.
await this.handleSendAgentMessage(agentId, result.text)
await this.flushPendingAudioSegments('transcription complete')
}
private registerVoiceBridgeForAgent(agentId: string): void {
@@ -6723,6 +6807,15 @@ export class Session {
this.bufferTimeout = setTimeout(async () => {
this.sessionLogger.debug('Buffer timeout reached, processing pending segments')
if (this.processingPhase === 'transcribing') {
this.sessionLogger.debug(
{ segmentCount: this.pendingAudioSegments.length },
'Buffer timeout deferred because transcription is still in progress'
)
this.setBufferTimeout()
return
}
if (this.pendingAudioSegments.length > 0) {
const segments = [...this.pendingAudioSegments]
this.pendingAudioSegments = []

View File

@@ -278,4 +278,5 @@ describe("voice turn controller", () => {
deferred.resolve();
await settleSerialQueue();
});
});

View File

@@ -117,6 +117,14 @@ export function createVoiceTurnController(params: {
rollingPrefixBytes: prefixBuffer.byteLength,
utteranceBytes: prefix.length,
};
params.logger.info(
{
utteranceId: state.utteranceId,
prefixBytes: prefix.length,
rollingPrefixBytes: prefixBuffer.byteLength,
},
"voice_turn.speech_started"
);
}
async function handleSpeechStopped(): Promise<void> {
@@ -133,6 +141,14 @@ export function createVoiceTurnController(params: {
await params.callbacks.onSpeechStopped();
params.logger.info(
{
utteranceBytes: utterance.length,
utteranceAgeMs: Math.max(0, endedAt - startedAt),
},
"voice_turn.speech_stopped"
);
if (utterance.length === 0) {
return;
}