Installation
Packages
npm install @micdrop/react-native react-native-audio-apiAudio is captured and played by react-native-audio-api, an implementation of the Web Audio API for React Native. It ships a player UI that pulls in three more packages, so install those as well:
npx expo install react-native-worklets react-native-gesture-handler react-native-reanimatedFor the React hooks:
npm install @micdrop/reactThe Silero voice detection needs onnxruntime-react-native as well,
which does not autolink on Expo SDK 57 today. The volume detection needs
nothing.
Native code is involved, so the app runs from a development build. Expo Go does not include these libraries.
Expo
Add the config plugin to app.json. It writes the microphone permission and the audio service into the native projects:
{ "expo": { "plugins": [ [ "react-native-audio-api", { "iosMicrophonePermission": "We need the microphone so you can talk with the assistant.", "androidPermissions": [ "android.permission.RECORD_AUDIO", "android.permission.MODIFY_AUDIO_SETTINGS", "android.permission.FOREGROUND_SERVICE", "android.permission.FOREGROUND_SERVICE_MICROPHONE", "android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" ], "androidFSTypes": ["microphone", "mediaPlayback"] } ] ] }}Then build once:
npx expo run:ios# ornpx expo run:androidLater runs only need npx expo start --dev-client.
Bare React Native
Add the microphone permission by hand.
ios/<App>/Info.plist:
<key>NSMicrophoneUsageDescription</key><string>We need the microphone so you can talk with the assistant.</string><key>UIBackgroundModes</key><array> <string>audio</string></array>android/app/src/main/AndroidManifest.xml:
<uses-permission android:name="android.permission.RECORD_AUDIO" /><uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />Then run pod install in ios/.
The permission itself is requested by Micdrop.start(), there is nothing to call beforehand.
Targeting phones only
Expo offers a web target unless you say otherwise, and bundling for it needs
react-native-web. An app meant for phones says so in app.json, which keeps
the dev server from failing on Unable to resolve react-native-web:
{ "expo": { "platforms": ["ios", "android"] }}Reaching the server
A phone does not share localhost with your machine, so a development server has to be reached by its address on the network. With Expo, the app already knows where it was loaded from:
import Constants from 'expo-constants'import { Platform } from 'react-native'
let host = Constants.expoConfig?.hostUri?.split(':')[0] ?? 'localhost'
// An Android emulator has its own loopback, so `localhost` there is the// emulator rather than the machine running the serverif (Platform.OS === 'android' && ['localhost', '127.0.0.1'].includes(host)) { host = '10.0.2.2'}
export const serverUrl = `ws://${host}:8087`Have the server listen on every interface (host: '0.0.0.0') so it accepts that connection.
An iOS simulator shares the machine’s network, and a phone on the same Wi-Fi gets
the address Expo advertised, so both work without the special case. A phone
plugged in over USB reaches the machine through adb reverse tcp:8087 tcp:8087.
In production, use wss:// and let the app talk to your deployed server.
Monorepo
Metro has to see the packages outside the app folder. In metro.config.js:
const path = require('path')const { getDefaultConfig } = require('expo/metro-config')
const projectRoot = __dirnameconst monorepoRoot = path.resolve(projectRoot, '../..')
const config = getDefaultConfig(projectRoot)config.watchFolders = [monorepoRoot]config.resolver.nodeModulesPaths = [ path.resolve(projectRoot, 'node_modules'), path.resolve(monorepoRoot, 'node_modules'),]
module.exports = configWith pnpm, leave hierarchical lookup on: each package finds its own dependencies through the store next to it.