expo-precision-metronome
v1.2.0
Published
High-precision metronome engine for Expo and React Native with native audio scheduling support.
Maintainers
Readme
expo-precision-metronome
High-precision metronome engine for Expo and React Native. Beats are scheduled at the native audio layer — timing stays rock-solid regardless of JS thread load.
Features
- Sample-accurate beat scheduling via AVAudioEngine (iOS) and Oboe (Android)
onBeatevent with beat index, high-resolution timestamp, and accent levelonStopevent distinguishing explicit stop from audio interruption (phone call, alarm, etc.)- Live BPM change without restarting the engine
- 6 synthesized sound presets switchable on the fly (
click,beep,woodblock,rim,hihat,cowbell) - Accent patterns — up to 16 beats, each independently
strong,normal, ormuted, changeable on the fly - JSI bridge — no JSON serialization overhead
- Full TypeScript types included
Requirements
| | Minimum | | ----------- | ------------------------------------------------ | | Expo SDK | 55 | | iOS | 15.1 | | Android API | 24 (26+ recommended for AAudio low-latency path) | | Node | 18 |
Installation
npx expo install expo-precision-metronome[!NOTE] This package requires native code. It does not work with Expo Go — use a development build.
Usage
import { useEffect } from "react";
import { start, stop, setBpm, setSound, setPattern } from "expo-precision-metronome";
import ExpoPrecisionMetronomeModule from "expo-precision-metronome";
export default function Metronome() {
useEffect(() => {
const beatSub = ExpoPrecisionMetronomeModule.addListener(
"onBeat",
({ beat, timestamp, accent }) => {
console.log(`Beat ${beat} (${accent}) at ${timestamp}s`);
},
);
const stopSub = ExpoPrecisionMetronomeModule.addListener("onStop", ({ reason }) => {
console.log(`Stopped: ${reason}`);
});
setSound("woodblock");
setPattern(["strong", "normal", "normal", "normal"]); // 4/4
start(120);
return () => {
stop();
beatSub.remove();
stopSub.remove();
};
}, []);
}Accent patterns
setPattern() defines a repeating accent pattern of up to 16 beats. Each beat is independently set to one of three levels:
| Level | Character |
| -------- | ------------------------------------------- |
| strong | Higher pitch, louder, punchier decay |
| normal | Standard click |
| muted | Ghost note — same timbre at ~12 % amplitude |
The pattern loops automatically. It takes effect immediately without restarting the engine.
import { setPattern } from "expo-precision-metronome";
// 4/4 — downbeat accent (this is the default)
await setPattern(["strong", "normal", "normal", "normal"]);
// 3/4 waltz
await setPattern(["strong", "normal", "normal"]);
// 6/8 compound time — accent on beats 1 and 4
await setPattern(["strong", "muted", "muted", "normal", "muted", "muted"]);
// Ghost groove — strong downbeat, ghost on beat 3
await setPattern(["strong", "normal", "muted", "normal"]);
// 16-step pattern (maximum length)
await setPattern([
"strong",
"muted",
"normal",
"muted",
"normal",
"muted",
"strong",
"muted",
"normal",
"muted",
"normal",
"muted",
"normal",
"muted",
"normal",
"muted",
]);
// Change on the fly while the engine is running
await setPattern(["strong", "normal", "normal"]); // switch to 3/4 mid-songAPI
Functions
start(bpm: number): Promise<void>
Starts the metronome at the given BPM. Resolves when the audio engine has started. Throws RangeError if bpm is outside BPM_MIN–BPM_MAX.
stop(): Promise<void>
Stops the metronome. Emits onStop with reason: "explicit".
setBpm(bpm: number): Promise<void>
Changes the tempo on the fly without stopping the engine. Throws RangeError if bpm is outside BPM_MIN–BPM_MAX.
setSound(sound: SoundPreset): Promise<void>
Switches the click sound without stopping the engine. The new preset takes effect on the next beat. Throws TypeError if sound is not one of the valid presets. Default is "click".
setPattern(pattern: BeatAccent[]): Promise<void>
Sets the accent pattern. pattern must contain 1–16 BeatAccent values. The pattern loops indefinitely — beat index 0 corresponds to the first element, beat index n to pattern[n % pattern.length]. Can be called while the engine is running; the new pattern takes effect from the next beat. Throws RangeError if the length is out of range, TypeError if any element is invalid. Default pattern is ["strong", "normal", "normal", "normal"].
Events
Subscribe via ExpoPrecisionMetronomeModule.addListener(eventName, handler). Always call .remove() on the returned subscription to avoid leaks.
onBeat
Emitted on every beat.
| Property | Type | Description |
| ----------- | ------------ | --------------------------------------------------------- |
| beat | number | Beat index, starting at 0, increments each beat |
| timestamp | number | High-resolution audio clock timestamp (seconds) |
| accent | BeatAccent | Accent level of this beat: strong, normal, or muted |
onStop
Emitted when the metronome stops for any reason.
| Property | Type | Description |
| -------- | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------- |
| reason | "explicit" \| "interruption" | "explicit" — stopped by stop(). "interruption" — stopped by the OS (incoming call, audio session interruption, etc.) |
Constants
| Constant | Value | Description |
| ------------------------- | ------------------------------------------------------ | ------------------------------------- |
| BPM_MIN | 20 | Minimum valid BPM |
| BPM_MAX | 300 | Maximum valid BPM |
| SOUND_PRESETS | ["click","beep","woodblock","rim","hihat","cowbell"] | All available sound presets |
| BEAT_ACCENTS | ["strong","normal","muted"] | All valid accent levels |
| BEAT_PATTERN_MAX_LENGTH | 16 | Maximum beats in a pattern |
| DEFAULT_BEAT_PATTERN | ["strong","normal","normal","normal"] | Default pattern used when none is set |
Types
type BeatAccent = "strong" | "normal" | "muted";
type BeatEventPayload = {
beat: number;
timestamp: number;
accent: BeatAccent;
};
type StopEventPayload = {
reason: "explicit" | "interruption";
};
type SoundPreset = "click" | "beep" | "woodblock" | "rim" | "hihat" | "cowbell";Accent levels
| Level | Volume | Pitch | Decay |
| -------- | -------- | -------------- | -------------- |
| strong | +30 % | ×1.4 freq | 0.6× faster |
| normal | baseline | baseline | baseline |
| muted | −88 % | same as normal | same as normal |
Sound presets
| Preset | Character | Duration |
| ----------- | ----------------------- | -------- |
| click | 1 kHz sine, fast decay | 10 ms |
| beep | 880 Hz sine, soft | 20 ms |
| woodblock | 400 Hz, very percussive | 8 ms |
| rim | 800 + 1600 Hz dual sine | 6 ms |
| hihat | Noise burst | 8 ms |
| cowbell | 562 + 845 Hz, long | 250 ms |
Running the example app
cd example
# iOS
npx expo run:ios
# Android
npx expo run:androidContributing
See CONTRIBUTING.md.
License
MIT © Andrey Kotlyar
