---
title: "Microphone"
canonical_url: https://developer.repebble.com/guides/events-and-services/microphone/
last_updated: 2026-11-04
sdk_version: 4.43
---
# Microphone

Source: https://developer.repebble.com/guides/events-and-services/microphone/

On the `emery`, `flint` and `gabbro` platforms, the Microphone API gives watchapps the raw audio, not just a transcription like [_Dictation_](https://developer.repebble.com/guides/events-and-services/dictation.md) does. There are two ways to use it:

- **Streaming to the phone** with [`mic_stream_to_phone_start()`](https://developer.repebble.com/docs/c/User_Interface/Microphone.md#mic_stream_to_phone_start "mic\_stream\_to\_phone\_start"). The watch compresses the audio and sends it to the app's PebbleKit JS, which receives 16 kHz, 16-bit mono PCM. Use this when the processing happens on the phone or in the cloud, for example speech recognition, recording or audio analysis.
- **Capturing on the watch** with [`mic_capture_start()`](https://developer.repebble.com/docs/c/User_Interface/Microphone.md#mic_capture_start "mic\_capture\_start"). The app receives 16 kHz, 16-bit mono PCM samples in a callback on the watch. Use this for processing that happens on the watch, for example level meters, tuners or clap detection.

The Microphone API needs SDK 4.44 and firmware 4.44.3 or later. It isn't available on the older `basalt`, `chalk` and `diorite` platforms, even though those watches have a microphone for dictation, or on `aplite`, which has no microphone.

## Requirements

Both parts of the API have the same rules:

- **Only foreground watchapps** can use the microphone. Watchfaces get [`MicStreamStartErrWatchface`](https://developer.repebble.com/docs/c/User_Interface/Microphone.md#MicStreamStartErrWatchface "MicStreamStartErrWatchface") or [`MicCaptureStartErrWatchface`](https://developer.repebble.com/docs/c/User_Interface/Microphone.md#MicCaptureStartErrWatchface "MicCaptureStartErrWatchface"), and background workers can't use the API.
- **Only one user at a time.** An app can stream or capture, but not both at once. While the system uses the microphone, for example for dictation, a start returns a `Busy` error.
- **The app must declare the `WatchMicrophone` permission** , and the user must allow it in the Pebble mobile app.
- **The system shows a "Listening" indicator** at the bottom of the screen while the microphone is on. This shrinks the unobstructed area, so lay out the app with [`layer_get_unobstructed_bounds()`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#layer_get_unobstructed_bounds "layer\_get\_unobstructed\_bounds"). See [The Listening Indicator](#the-listening-indicator).
- **The microphone stops when the app loses focus** , for example when a notification appears. The app is told why through its `stopped` handler. Starting again is up to the app.

## Declaring the Permission

Add `WatchMicrophone` to `usesPermissions` in the `pebble` block of `package.json`:

```
"pebble": {"usesPermissions": ["WatchMicrophone"]}
```

Permissions need `manifestVersion` 2, which is the default since SDK 4.44. Don't set `"manifestVersion": 1`: the phone never allows the microphone for a version 1 app. The build warns when an app calls [`mic_capture_start()`](https://developer.repebble.com/docs/c/User_Interface/Microphone.md#mic_capture_start "mic\_capture\_start") or [`mic_stream_to_phone_start()`](https://developer.repebble.com/docs/c/User_Interface/Microphone.md#mic_stream_to_phone_start "mic\_stream\_to\_phone\_start") without declaring the permission.

The Pebble mobile app only allows the microphone when permissions are turned on ( **Settings → Plugins → Plugins / permissions preview** , see [_Pebble Plugins Preview_](https://developer.repebble.com/guides/plugins/preview.md)). Without that, every stream and capture is refused. Depending on the user's permission settings, the microphone is allowed straight away or needs the user's approval. A start doesn't wait for an answer: while the permission still needs approval, the start is refused, and the user can allow it on the app's page in the Pebble app. The user can take the permission away at any time, which also stops a running stream or capture.

For capture, the phone syncs the grant to the watch, where it stays until the phone updates it, so capture works without a phone connection once the grant has been synced.

## Streaming to PebbleKit JS

### Starting a Stream on the Watch

Call [`mic_stream_to_phone_start()`](https://developer.repebble.com/docs/c/User_Interface/Microphone.md#mic_stream_to_phone_start "mic\_stream\_to\_phone\_start") with a [`MicStreamHandlers`](https://developer.repebble.com/docs/c/User_Interface/Microphone.md#MicStreamHandlers "MicStreamHandlers"). The `stopped` handler is required. The `started` handler is optional and is called once the phone accepts the stream:

```
static void stream\_started(void \*context) {text\_layer\_set\_text(s\_status\_layer, "Listening...");}static void stream\_stopped(MicStreamStopReason reason, void \*context) {switch (reason) {case MicStreamStopReasonFocusLost:text\_layer\_set\_text(s\_status\_layer, "Paused");break;case MicStreamStopReasonPhone:text\_layer\_set\_text(s\_status\_layer, "Phone ended the stream");break;default:text\_layer\_set\_text(s\_status\_layer, "Stopped");break;}}static void start\_streaming(void) {const MicStreamStartResult result = mic\_stream\_to\_phone\_start((MicStreamHandlers){.started = stream\_started,.stopped = stream\_stopped,},NULL);if (result != MicStreamStartOk) {APP\_LOG(APP\_LOG\_LEVEL\_WARNING, "Couldn't stream: %d", (int)result);}}
```

[`MicStreamStartOk`](https://developer.repebble.com/docs/c/User_Interface/Microphone.md#MicStreamStartOk "MicStreamStartOk") only means the request was sent. The phone can still refuse it: when the user hasn't allowed the microphone, when the app has no PebbleKit JS, or when its PebbleKit JS hasn't fired `ready` within about 6 seconds. In that case `stopped` is called with [`MicStreamStopReasonPhone`](https://developer.repebble.com/docs/c/User_Interface/Microphone.md#MicStreamStopReasonPhone "MicStreamStopReasonPhone").

Call [`mic_stream_to_phone_stop()`](https://developer.repebble.com/docs/c/User_Interface/Microphone.md#mic_stream_to_phone_stop "mic\_stream\_to\_phone\_stop") to end the stream. Stopping it yourself doesn't call `stopped`. [`mic_stream_to_phone_is_active()`](https://developer.repebble.com/docs/c/User_Interface/Microphone.md#mic_stream_to_phone_is_active "mic\_stream\_to\_phone\_is\_active") returns `true` from a successful start until the stream ends.

The `stopped` handler tells the app why the stream ended:

| Reason | Meaning |
| --- | --- |
| [`MicStreamStopReasonStopped`](https://developer.repebble.com/docs/c/User_Interface/Microphone.md#MicStreamStopReasonStopped "MicStreamStopReasonStopped") | The app stopped the stream. |
| [`MicStreamStopReasonFocusLost`](https://developer.repebble.com/docs/c/User_Interface/Microphone.md#MicStreamStopReasonFocusLost "MicStreamStopReasonFocusLost") | The app lost focus, for example to a notification. |
| [`MicStreamStopReasonInterrupted`](https://developer.repebble.com/docs/c/User_Interface/Microphone.md#MicStreamStopReasonInterrupted "MicStreamStopReasonInterrupted") | The system took the microphone, for example for dictation. |
| [`MicStreamStopReasonPhone`](https://developer.repebble.com/docs/c/User_Interface/Microphone.md#MicStreamStopReasonPhone "MicStreamStopReasonPhone") | The phone refused, stopped or lost the stream. |
| [`MicStreamStopReasonError`](https://developer.repebble.com/docs/c/User_Interface/Microphone.md#MicStreamStopReasonError "MicStreamStopReasonError") | An unexpected error. |

### Receiving the Audio in PebbleKit JS

The phone only accepts a stream for the running app's PebbleKit JS, once it has fired `ready`. The audio arrives as three events:

| Event | Properties | Meaning |
| --- | --- | --- |
| `audiostreamstart` | `sampleRate` (`16000`), `format` (`'pcm16le'`) | The stream has started. |
| `audiostream` | `data` (`ArrayBuffer`) | About 100 ms of audio. |
| `audiostreamstop` | | The stream has ended. Any remaining audio was delivered first. |

`data` holds signed 16-bit little-endian mono samples, so read it with an `Int16Array`:

```
var chunks = [];Pebble.addEventListener('audiostreamstart', function(e) {console.log('Streaming at ' + e.sampleRate + ' Hz');chunks = [];});Pebble.addEventListener('audiostream', function(e) {var samples = new Int16Array(e.data);chunks.push(samples);});Pebble.addEventListener('audiostreamstop', function(e) {console.log('Received ' + chunks.length + ' chunks');// Send the recording to a server, run speech recognition, etc.});
```

To reach a server with the audio, the app also needs to declare `Internet` for that domain in `usesPermissions`.

The watch compresses the audio with Speex at about 10 kbps before sending it to the phone, which is tuned for speech.

## Capturing on the Watch

### Starting Capture

Call [`mic_capture_start()`](https://developer.repebble.com/docs/c/User_Interface/Microphone.md#mic_capture_start "mic\_capture\_start") with a [`MicCaptureHandlers`](https://developer.repebble.com/docs/c/User_Interface/Microphone.md#MicCaptureHandlers "MicCaptureHandlers"). Both handlers are required. Capture starts right away when the call succeeds:

```
static void capture\_data(const int16\_t \*samples, size\_t sample\_count,void \*context) {// Find the peak level of this 20 ms frameint32\_t peak = 0;for (size\_t i = 0; i \< sample\_count; i++) {const int32\_t level = abs(samples[i]);if (level \> peak) {peak = level;}}s\_peak = peak;layer\_mark\_dirty(s\_meter\_layer);}static void capture\_stopped(MicCaptureStopReason reason, void \*context) {if (reason == MicCaptureStopReasonPermissionRevoked) {text\_layer\_set\_text(s\_status\_layer, "Microphone not allowed");}}static void start\_capture(void) {const MicCaptureStartResult result = mic\_capture\_start((MicCaptureHandlers){.data = capture\_data,.stopped = capture\_stopped,},NULL);if (result == MicCaptureStartErrPermissionDenied) {text\_layer\_set\_text(s\_status\_layer,"Allow the microphone in the Pebble app");} else if (result != MicCaptureStartOk) {APP\_LOG(APP\_LOG\_LEVEL\_WARNING, "Couldn't capture: %d", (int)result);}}
```

The `data` handler runs on the app task and receives 320 samples (20 ms at [`MIC_CAPTURE_SAMPLE_RATE`](https://developer.repebble.com/docs/c/User_Interface/Microphone.md#MIC_CAPTURE_SAMPLE_RATE "MIC\_CAPTURE\_SAMPLE\_RATE"), 16 kHz) each time. The samples are only valid during the callback, so copy them if they're needed later. If the app takes too long to handle a frame, later frames are dropped, so keep the handler short.

Call [`mic_capture_stop()`](https://developer.repebble.com/docs/c/User_Interface/Microphone.md#mic_capture_stop "mic\_capture\_stop") to end capture. Stopping it yourself doesn't call `stopped`, and it's safe to call when nothing is being captured. [`mic_capture_is_active()`](https://developer.repebble.com/docs/c/User_Interface/Microphone.md#mic_capture_is_active "mic\_capture\_is\_active") returns whether capture is running.

### Start Errors and Stop Reasons

[`mic_capture_start()`](https://developer.repebble.com/docs/c/User_Interface/Microphone.md#mic_capture_start "mic\_capture\_start") returns one of:

| Result | Meaning |
| --- | --- |
| [`MicCaptureStartOk`](https://developer.repebble.com/docs/c/User_Interface/Microphone.md#MicCaptureStartOk "MicCaptureStartOk") | Capture has started. |
| [`MicCaptureStartErrBusy`](https://developer.repebble.com/docs/c/User_Interface/Microphone.md#MicCaptureStartErrBusy "MicCaptureStartErrBusy") | The microphone is already in use, by this app or the system. |
| [`MicCaptureStartErrNotForeground`](https://developer.repebble.com/docs/c/User_Interface/Microphone.md#MicCaptureStartErrNotForeground "MicCaptureStartErrNotForeground") | The app isn't in the foreground. |
| [`MicCaptureStartErrInvalidArgs`](https://developer.repebble.com/docs/c/User_Interface/Microphone.md#MicCaptureStartErrInvalidArgs "MicCaptureStartErrInvalidArgs") | A handler is missing. |
| [`MicCaptureStartErrWatchface`](https://developer.repebble.com/docs/c/User_Interface/Microphone.md#MicCaptureStartErrWatchface "MicCaptureStartErrWatchface") | Watchfaces can't capture. |
| [`MicCaptureStartErrUnavailable`](https://developer.repebble.com/docs/c/User_Interface/Microphone.md#MicCaptureStartErrUnavailable "MicCaptureStartErrUnavailable") | The platform doesn't support the Microphone API. |
| [`MicCaptureStartErrPermissionDenied`](https://developer.repebble.com/docs/c/User_Interface/Microphone.md#MicCaptureStartErrPermissionDenied "MicCaptureStartErrPermissionDenied") | The user hasn't allowed the microphone, or the grant hasn't been synced yet. |
| [`MicCaptureStartErrNoMemory`](https://developer.repebble.com/docs/c/User_Interface/Microphone.md#MicCaptureStartErrNoMemory "MicCaptureStartErrNoMemory") | Not enough memory to start capture. |

The `stopped` handler is only called when capture ends without the app asking:

| Reason | Meaning |
| --- | --- |
| [`MicCaptureStopReasonFocusLost`](https://developer.repebble.com/docs/c/User_Interface/Microphone.md#MicCaptureStopReasonFocusLost "MicCaptureStopReasonFocusLost") | The app lost focus, for example to a notification. |
| [`MicCaptureStopReasonInterrupted`](https://developer.repebble.com/docs/c/User_Interface/Microphone.md#MicCaptureStopReasonInterrupted "MicCaptureStopReasonInterrupted") | The system took the microphone, for example for dictation. |
| [`MicCaptureStopReasonPermissionRevoked`](https://developer.repebble.com/docs/c/User_Interface/Microphone.md#MicCaptureStopReasonPermissionRevoked "MicCaptureStopReasonPermissionRevoked") | The user took away the microphone permission. |

## The Listening Indicator

While the microphone is on, the system shows an indicator at the bottom of the screen, so the user always knows that the app is listening. The app can't cover or dismiss it.

#### emery

#### flint

#### gabbro

 ![](https://developer.repebble.com/assets/images/guides/sensors-and-input/microphone/memo~emery.gif)

 ![](https://developer.repebble.com/assets/images/guides/sensors-and-input/microphone/memo~flint.gif)

 ![](https://developer.repebble.com/assets/images/guides/sensors-and-input/microphone/memo~gabbro.gif)

- On `emery` and `flint`, a "Listening..." bar slides up from the bottom of the screen. After 3 seconds it shrinks into a square in the bottom-left corner, which stays until the microphone stops.
- On `gabbro`, a red dome with a microphone icon rises from the bottom edge and stays as it is.

The indicator reserves the bottom of the screen through the unobstructed area, the same way Timeline Quick View does on watchfaces. The reserved height doesn't change when the bar shrinks, so an app laid out with [`layer_get_unobstructed_bounds()`](https://developer.repebble.com/docs/c/User_Interface/Layers.md#layer_get_unobstructed_bounds "layer\_get\_unobstructed\_bounds") moves once when the microphone starts and once when it stops. The app redraws automatically whenever the unobstructed area changes. See [_Unobstructed Area_](https://developer.repebble.com/guides/user-interfaces/unobstructed-area.md).

### Showing a Status Next to the Indicator

Once the bar has shrunk into the corner, the rest of the reserved strip shows the app again. [`layer_get_mic_accessory_bounds()`](https://developer.repebble.com/docs/c/User_Interface/Microphone.md#layer_get_mic_accessory_bounds "layer\_get\_mic\_accessory\_bounds") returns that area, to the right of the square, in a layer's coordinates. It's the place for a short status, such as the elapsed time and level meter in the memo app above:

```
static void canvas\_update\_proc(Layer \*layer, GContext \*ctx) {// The main layout goes in the unobstructed areaconst GRect bounds = layer\_get\_unobstructed\_bounds(layer);draw\_title(ctx, bounds);GRect accessory;if (layer\_get\_mic\_accessory\_bounds(layer, &accessory)) {// Next to the indicator, with some padding from the squaredraw\_status(ctx, grect\_inset(accessory, GEdgeInsets(0, 8)));} else if (mic\_capture\_is\_active()) {// No room next to the indicator, as on gabbro: use the main layoutdraw\_status(ctx, GRect(bounds.origin.x, bounds.origin.y + bounds.size.h - 40,bounds.size.w, 30));}}
```

- Draw in the area whenever the function returns `true`. The full bar covers it at first and uncovers it as it shrinks, so there's nothing to time.
- The area keeps the bar's height while the indicator slides in and out, so what's drawn in it moves with the indicator. The app redraws on every step, as it does for any unobstructed area change.
- It returns `true` from the moment the microphone starts until the indicator has slid away, so an app that falls back to its main layout when it returns `false` never shows the status in both places.
- The area is clipped to the layer's sides, but not to its top or bottom. A layer next to an [`ActionBarLayer`](https://developer.repebble.com/docs/c/User_Interface/Layers/ActionBarLayer.md "ActionBarLayer") gets the part up to the action bar.
- On `gabbro` it always returns `false`, because the dome leaves no room beside it. Show the status in the main layout instead, as in the `else` branch above.
- The area is small, about 160 by 38 pixels on `emery` and 114 by 30 pixels on `flint`, so keep the content short.

## Detecting Microphone Support

The Microphone API only works on `emery`, `flint` and `gabbro`. Checking `PBL_MICROPHONE` isn't enough on its own, because it's also defined on `basalt`, `chalk` and `diorite`, which only support dictation. Those platforms also define `PBL_SDK_FROZEN`, so check both at compile time:

```
#if defined(PBL\_MICROPHONE) && !defined(PBL\_SDK\_FROZEN)start\_capture();#elsetext\_layer\_set\_text(s\_status\_layer, "No microphone");#endif
```

On the other platforms the functions still compile, but [`mic_stream_to_phone_start()`](https://developer.repebble.com/docs/c/User_Interface/Microphone.md#mic_stream_to_phone_start "mic\_stream\_to\_phone\_start") and [`mic_capture_start()`](https://developer.repebble.com/docs/c/User_Interface/Microphone.md#mic_capture_start "mic\_capture\_start") always return [`MicStreamStartErrUnavailable`](https://developer.repebble.com/docs/c/User_Interface/Microphone.md#MicStreamStartErrUnavailable "MicStreamStartErrUnavailable") and [`MicCaptureStartErrUnavailable`](https://developer.repebble.com/docs/c/User_Interface/Microphone.md#MicCaptureStartErrUnavailable "MicCaptureStartErrUnavailable"), so apps that call the API on every platform can check the return value instead.

## Testing in the Emulator

The emulator supports both streaming and capture with pebble-tool 5.1.0 or later. Use `pebble emu-mic` to feed audio into the emulated microphone while the app listens:

```
$ pebble emu-mic # a 440 Hz tone$ pebble emu-mic --tone 1000 # a 1 kHz tone$ pebble emu-mic --noise # white noise$ pebble emu-mic --file speech.wav # a WAV file; add --loop to repeat it$ pebble emu-mic --live # your computer's microphone
```

The emulator allows `WatchMicrophone` for every app that declares it. Use `pebble emu-permission` to try out a refusal, or the user taking away the permission while the app is listening:

```
$ pebble emu-permission WatchMicrophone --deny$ pebble emu-permission WatchMicrophone --grant$ pebble emu-permission WatchMicrophone --reset
```

`--live` needs pebble-tool's optional `mic` extra, and on Linux also `libportaudio2`.

## Battery Considerations

Keeping the microphone on uses a lot of power. Streaming also keeps the Bluetooth connection busy. Only listen while the user is actively using the feature, and stop the microphone as soon as it's no longer needed.
