Microphone plugin for Tauri v2: cross-platform audio recording, live metering and raw PCM streaming on Android, iOS, macOS, Windows and Linux behind a single JavaScript API.
Development note (AI assistance). This plugin is being developed with the assistance of AI tools. AI may have generated parts of the code, tests and documentation. Every change is reviewed and verified by maintainers (automated tests and CI) before release, but we disclose this for transparency. Please report any issue you find.
# src-tauri/Cargo.toml
[dependencies]
tauri-plugin-microphone-kessdev = "0.1"npm install tauri-plugin-microphone-kessdev// src-tauri/src/lib.rs
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_microphone_kessdev::init())
.run(tauri::generate_context!())
.expect("error while running tauri application");
}- Desktop: the guaranteed native path builds
cpal(Linux needslibasound2-dev; the other platforms use their system audio backend) andopus-head-sys, which compiles the vendored libopus with CMake (a C toolchain is required). FLAC uses the pure-Rustflacenc, no system library. - Android: declare
android.permission.RECORD_AUDIO(the plugin manifest already does). - iOS/macOS: add
NSMicrophoneUsageDescriptiontoInfo.plist.
import {
startRecording,
stopRecording,
pauseRecording,
resumeRecording,
getRecordingState,
onMeter,
startAudioStream,
stopAudioStream,
checkPermissions,
requestPermissions,
openAppSettings,
getCapabilities
} from 'tauri-plugin-microphone-kessdev'
await requestPermissions()
await startRecording({ format: 'wav' })
// mobile & desktop native formats -> { duration, format, path }
// desktop webview formats -> { duration, format, blob, url }
const clip = await stopRecording()| Function | Description |
|---|---|
startRecording(options?) |
Start recording. |
stopRecording() |
Stop and return the AudioClip. |
pauseRecording() |
Pause without including the paused interval. |
resumeRecording() |
Resume a paused recording. |
getRecordingState() |
idle | recording | paused. |
onMeter(cb, options?) => UnlistenFn |
Live level updates. |
getAnalyserNode() |
Desktop: the AnalyserNode for custom visualizers. |
startAudioStream(options, onChunk) |
Raw PCM frames over a Channel. |
stopAudioStream() |
Stop the stream. |
checkPermissions() |
OS microphone permission state. |
requestPermissions() |
Request OS microphone permission. |
openAppSettings() |
Open app settings (mobile). |
getCapabilities() |
Supported formats, rates, channels and flags. |
getSupportedFormats() |
List of supported output formats. |
The Tauri commands backing these functions are declared in build.rs and
enumerated in permissions/default.toml.
interface RecordingOptions {
format?: 'wav' | 'm4a' | 'aac' | 'flac' | 'opus' // preference; real format is reported
sampleRate?: number // Hz (default 48000)
channels?: number // 1 or 2 (default 1)
bitRate?: number // bits/s, for compressed formats
maxDuration?: number // seconds; automatically stops the recording
deviceId?: string // desktop input device name
background?: boolean // mobile only; desktop rejects with an error
}Unknown deviceId on desktop rejects with a descriptive error. checkPermissions
and requestPermissions resolve to 'granted' | 'denied' | 'prompt'.
const unlisten = onMeter((data) => console.log(data.level), {
intervalMs: 100, // update period in ms
scale: 'linear', // 'linear' (0..1) or 'dbfs' (<= 0)
floorDb: -60, // dB floor when scale is 'dbfs'
peakHold: true, // hold the peak with decay
spectrum: true // include frequency bands (where supported)
})
unlisten()MeterData:
interface MeterData {
level: number // 0..1 (linear) or dBFS (<= 0)
peak?: number // 0..1
rms?: number // 0..1
db?: number // dBFS
clipping?: boolean
spectrum?: number[] // normalized band magnitudes (where supported)
timestamp: number // seconds
}Recommended intervalMs: 100 ms for meters and VU displays, 33–50 ms
(~20–30 fps) for smooth visualizers. Going below ~16 ms adds work without
visible benefit, since updates are bounded by the display refresh rate.
On desktop metering uses a Web Audio AnalyserNode (zero IPC). Use
getAnalyserNode() to obtain it for custom visualization; it returns null on
mobile, where the native backend computes and delivers MeterData instead.
Desktop metering is bound to the webview capture path (getUserMedia +
MediaRecorder): the AnalyserNode only exists while that recording is active,
so onMeter works whether it is called before or after startRecording (the
buffer is sized lazily) and getAnalyserNode() returns the node while recording.
When the desktop native path is used (format: 'wav' | 'flac' | 'opus'),
there is no AnalyserNode: onMeter does not emit and getAnalyserNode()
returns null.
interface AudioClip {
duration: number // seconds
format: 'wav' | 'm4a' | 'aac' | 'flac' | 'opus' | string // real format
path?: string // mobile, and desktop native path
blob?: Blob // desktop
url?: string // desktop object URL (revoke after use)
}- Mobile: the native backend writes a temporary file and returns
path. - Desktop: requesting a non-native format uses the webview
MediaRecorderand returnsblob+url. Requesting a native format (wav,flac,opus) returnspath; the facade also materializesblob+urlwhen the Tauri asset protocol is enabled (app.security.assetProtocol.enable = trueand a matching scope), and otherwise returnspathonly. formatis a preference:AudioClip.formatalways reports the format actually produced.
const stream = await startAudioStream({ sampleRate: 48000, frameSamples: 1024 },
(chunk) => {
// chunk.data: ArrayBuffer of interleaved 16-bit PCM
// chunk.sequence, chunk.timestamp, chunk.droppedBefore?
myWasmVad.process(chunk.data)
})
// ...
await stream.stop()- The stream always carries raw PCM (16-bit, interleaved), independent of the
file format used for
startRecording. - Desktop uses transport
raw: eachChannelframe is a binary[sequence: u64 LE][timestamp: f64 LE][pcm: i16 LE ...]in a singleArrayBuffer. Inter-frame gaps insequenceare surfaced asdroppedBefore. - Mobile uses transport
base64: eachChannelmessage is{ sequence, timestamp, data: base64(pcm) }, normalized by the facade. info.transport('raw'|'base64') tells you which transport is in use.- Backpressure: the desktop capture keeps a bounded queue (64 frames) and
drops the oldest frame when the consumer lags, reporting the drop as a
sequencegap. PassmaxSecondsto auto-stop after a duration. - Commands
start_audio_stream/stop_audio_streamare gated by theallow-start-audio-stream/allow-stop-audio-streampermissions (included in the default set).
const caps = await getCapabilities()
// {
// formats: ['wav', ...], // output formats available on this platform
// preferredFormat: 'wav', // best default choice
// sampleRates: [44100, 48000],
// channels: [1, 2],
// supportsPause: true,
// supportsBackground: false,
// supportsSpectrum: true,
// supportsAudioStream: true,
// streamTransport: 'raw' // 'raw' (desktop) | 'base64' (mobile)
// }Use capabilities to pick a format instead of hardcoding:
- If the caller has no explicit preference, request
caps.preferredFormat(alwayswavtoday, guaranteed everywhere). - Otherwise, if
caps.formatscontains the requested format, request it; the effective format is confirmed inAudioClip.format. - If it is not listed, request
wav(or accept the documented degradation) to avoid an invalid file.
The formats list mirrors the AudioFormat union in TypeScript and the Rust
AudioFormat enum, and is reflected by getSupportedFormats() and the tests.
| Format | Android | iOS | Windows | Linux | macOS |
|---|---|---|---|---|---|
| WAV/PCM | AudioRecord + WAV header | AVAudioRecorder LPCM | cpal + hound | cpal + hound | cpal + hound |
| FLAC | flacenc (Rust, in-process) |
flacenc (Rust, in-process) |
flacenc |
flacenc |
flacenc |
| Opus | MediaRecorder OGG (API 29+) | pending (libopus FFI) | vendored libopus | vendored libopus | vendored libopus |
| M4A/AAC | MediaRecorder | AVAudioRecorder (AAC) | webview MediaRecorder |
webview MediaRecorder |
webview MediaRecorder |
WAV/PCM is guaranteed on every platform. Other formats are progressive; a —
means the platform does not produce that format today, and requesting it degrades
to WAV and reports the effective format instead of returning an invalid file.
getSupportedFormats() / getCapabilities() are the source of truth for the
current platform.
- Android records M4A/AAC natively (
MediaRecorder, MPEG-4 + AAC). - iOS records M4A/AAC through the system codec (
AVAudioRecorder, AAC). - Windows/Linux/macOS desktop: the webview
MediaRecordermay produceaudio/mp4(AAC); when it does, the clip reportsformat: 'aac'. AAC encoders on Windows/Linux carry their own licensing (AAC is patent-encumbered; e.g.fdk-aachas its own terms), which is not covered by this project, so the guaranteed native desktop path does not produce AAC. CheckgetCapabilities().formatsbefore requesting M4A/AAC on desktop.
FLAC is lossless compressed audio: it preserves the exact PCM samples of WAV
at roughly half the size. It is produced on all platforms by the same
pure-Rust flacenc encoder (no C dependency): on desktop inside the native
capture path, and on Android/iOS the native capture hands the PCM to that same
in-process encoder (Option B). The PCM is buffered in memory and encoded when
recording stops, so very long recordings use memory proportional to their length.
Because it is lossless, FLAC is the preferred archival alternative to WAV when
size matters.
On iOS the in-process encoder is provided by the app's Rust static library and
resolved at runtime; if those symbols are not linked (for example when the Swift
package is built on its own), an iOS FLAC request degrades to WAV and reports
format: 'wav' instead of returning an invalid file.
- Android encodes Ogg/Opus natively with
MediaRecorder(OGG container +OPUSencoder), available on API 29+; on older APIs an Opus request degrades to WAV and reportsformat: 'wav'. - Desktop encodes Ogg/Opus with the vendored libopus C library
(
opus-head-sys, compiled via CMake) and writes a valid Ogg/Opus container. This adds a C build step (needs a C toolchain and CMake) but no system libopus install. - iOS Opus is implemented but pending: it requires packaging
libopusas an xcframework and linking aCOpusSwiftPM module. Until that is done the code is inactive, iOS does not advertise Opus andgetSupportedFormats()omits it (see theadd-ios-opus-testschange).
Opus is lossy and low-latency (ideal for voice/streaming); use WAV/FLAC when lossless fidelity is required.
Two independent layers:
- OS permission —
checkPermissions(),requestPermissions(),openAppSettings(). On desktop, requesting isgetUserMedia({ audio: true })andopenAppSettings()rejects with a descriptive error. - Tauri capabilities —
permissions/default.tomllists theallow-*permissions for every command. Plugin events (used byonMeteron mobile) are gated through theregister_listener/remove_listenercommands, so the default set includesallow-register-listenerandallow-remove-listener. Add the default set to your capability:
{
"permissions": ["microphone-kessdev:default"]
}The OS permission and the Tauri capability are independent: granting the
capability lets the webview call the commands, but the OS may still deny access
to the microphone (and checkPermissions()/requestPermissions() report that).
The listener capability is required for onMeter to receive meter events.
Platform configuration:
- Android: declare
android.permission.RECORD_AUDIO(the plugin manifest already does). - iOS/macOS: add
NSMicrophoneUsageDescriptiontoInfo.plist.
Pass background: true to startRecording to keep recording while the app is in
the background.
- Android starts a foreground service of type
microphone. The plugin manifest declares:android.permission.RECORD_AUDIOandroid.permission.FOREGROUND_SERVICEandroid.permission.FOREGROUND_SERVICE_MICROPHONEandroid.permission.POST_NOTIFICATIONS(Android 13+; needed to show the persistent notification, request it at runtime).
- iOS requires
UIBackgroundModes=audioinInfo.plist; the plugin sets theAVAudioSessioncategory toplayAndRecordand callssetActive(true). - Desktop does not support background recording:
getCapabilities().supportsBackground === falseand requestingbackground: truerejects with a descriptive error.
Info.plist keys used by the plugin:
NSMicrophoneUsageDescription— microphone access.UIBackgroundModes(containingaudio) — background recording.
# JavaScript
npm install && npm test && npm run build
# Rust (desktop)
cargo test && cargo clippy --all-targets -- -D warnings
# Example app (desktop)
cd examples/tauri-app && npx tauri dev
# Native suites (need Android SDK/NDK or Xcode)
cd examples/tauri-app && npx tauri android init && npx tauri android build --debug
cd examples/tauri-app/src-tauri/gen/android && ./gradlew test # Kotlin
cd ios && xcodebuild test -scheme tauri-plugin-microphone # SwiftMIT