🎤 Micdrop

Upgrade to v3

Released in August 2026.

v3 splits the client in two. Everything that runs a call, the protocol, the state, the voice activity detection and the recording, moved into a platform agnostic @micdrop/client. The browser code that surrounds it became @micdrop/web, and @micdrop/react-native is the same call on iOS and Android.

For most apps the upgrade is one install and one search and replace.

Which versions go together

Packagev2v3
@micdrop/web1.xnew, this is what a browser app installs
@micdrop/react-native0.xnew, for iOS and Android
@micdrop/client2.x3.xno longer installed directly, it arrives as a dependency
@micdrop/react1.x2.xnow works on both platforms
@micdrop/server2.x2.xunchanged, nothing to do

The server does not move. The protocol between the two sides is the same, so a v3 client talks to the server you are already running, and you can upgrade one side at a time.

The AI packages, @micdrop/openai and the others, do not move either. They sit on the server and never saw the client.

The short version

Terminal window
npm uninstall @micdrop/client
npm install @micdrop/web
import { Micdrop } from '@micdrop/client'
import { Micdrop } from '@micdrop/web'

@micdrop/web re-exports the whole shared API, so every name you imported before still comes from this single package. You never install @micdrop/client yourself, it arrives as a dependency and follows the browser package.

If you use the React hooks, upgrade them too:

Terminal window
npm install @micdrop/react@^2

@micdrop/react v2 declares @micdrop/client v3 as a peer, so npm says so if the two drift apart. Kept together by force, useMicVolume and useSpeakerVolume subscribe to a Volume event that v2 never emits, and the level meters stay flat. The other four hooks would still work.

What breaks

Devices are one shape on every platform

A phone does not have MediaDeviceInfo, so devices became a small type of their own. The values are the same, the field names are shorter.

<option key={device.deviceId} value={device.deviceId}>
{device.label || 'Microphone'}
<option key={device.id} value={device.id}>
{device.label}
</option>

MicdropDevice carries id, label and kind. The label is never empty now, it falls back to a generic name when the browser hides it.

The speaker takes samples rather than a Blob

Speaker.playAudio() used to accept the Blob the WebSocket delivered. The connection now reads binary as ArrayBuffer, which skips a conversion on every chunk, so the speaker takes the samples directly.

const audioBlob = await response.blob()
Speaker.playAudio(audioBlob)
Speaker.playAudio(await response.arrayBuffer())

Int16Array works too. Nothing changes for a normal call, where the client feeds the speaker on its own.

Levels come from events, not from an analyser

Mic.analyser and Speaker.analyser are gone, along with the AudioAnalyser class. The level is now an event, measured the same way on both platforms.

Mic.analyser.on('volume', onVolume)
Mic.on('Volume', onVolume)

The number means exactly what it meant before, the loudest frequency in decibels, so a threshold you had tuned still holds.

For a raw AnalyserNode, connect one to the node the assistant voice comes out of:

import { audioContext, getSpeakerOutput } from '@micdrop/web'
const analyser = audioContext.createAnalyser()
getSpeakerOutput()?.connect(analyser)

The microphone no longer hands back a stream

Mic.start() returns nothing. A phone has no MediaStream, and the recorder reads the microphone through the shared object instead.

const stream = await Mic.start()
await recorder.start(stream)
await Mic.start()
await recorder.start(Mic)

Silero is an opt-in import

The ONNX runtime weighs more than the rest of Micdrop put together, so it only enters your bundle when you ask for it. Add one import, wherever your app starts:

import '@micdrop/web/silero'

Without it, vad: 'silero' throws with a message saying exactly this. 'volume' needs nothing.

Renamed

v2v3
LocalStorageKeysMicdropStorageKeys
state.micDevices: MediaDeviceInfo[]state.micDevices: MicdropDevice[]

The stored keys themselves are unchanged, so a returning user keeps the microphone, the speaker and the VAD settings they had.

Errors say what went wrong

A missing url and a refused microphone used to surface as MicdropClientErrorCode.Connection, because they were caught and relabelled on the way out. They now arrive as MissingUrl and Mic. Code that branched on Connection to cover those cases needs the other two branches.

What does not change

  • The protocol. A v3 client talks to a v2 server, and the other way round. @micdrop/server did not move.
  • The call API. start, stop, mute, pause, StateChange, the conversation, the tool calls: all identical.
  • Saved settings. Same storage keys, same values.
  • VAD thresholds. The level is measured exactly as before, so your tuned numbers carry over.

What you gain

  • The same call on iOS and Android, against the server you already run
  • React hooks that work on both, from one package
  • Silero on a phone as well as in a browser, sharing one implementation
  • A first syllable that no longer gets cut: each VAD now declares how long it takes to notice speech, and that much audio is kept in reserve
  • A warning when a microphone announces one sample rate and delivers another, which used to turn into unexplained transcription errors