react-native-tuner-engine
v1.4.1
Published
A high-performance React Native Turbo Module for real-time instrument tuning with native audio processing and pitch detection.
Downloads
1,408
Maintainers
Readme
react-native-tuner-engine
A React Native Turbo Module for real-time instrument pitch detection. The detection pipeline runs entirely in C++ on a dedicated audio thread and delivers per-frame results to JavaScript via the New Architecture event system.
Requires React Native 0.75 or later with the New Architecture enabled.
How it works
Audio is captured through platform-native APIs (AVAudioEngine on iOS, Oboe on Android) and fed into a lock-free SPSC ring buffer. A C++ worker thread drains the buffer in fixed-size frames, runs the signal through a preprocessing pipeline, then through two complementary pitch detectors: probabilistic YIN (pYIN) as the precise primary estimator and a cepstrum detector as an independent corroborator. Their fused result fires an onPitch event on the JS thread.
The audio callback allocates nothing at runtime — all working buffers are pre-allocated during initialization.
Installation
npm install react-native-tuner-engine
# or
yarn add react-native-tuner-engineExpo
Works with Expo development builds (not Expo Go). Add the plugin to your app.json:
{
"expo": {
"plugins": ["react-native-tuner-engine"]
}
}To customize the iOS microphone permission message:
{
"expo": {
"plugins": [
["react-native-tuner-engine", {
"microphonePermission": "Microphone access is required for pitch detection."
}]
]
}
}Then rebuild with npx expo prebuild or EAS Build.
Bare React Native — iOS
Add a microphone usage description to your app's Info.plist:
<key>NSMicrophoneUsageDescription</key>
<string>Microphone access is required for pitch detection.</string>Then run pod install.
Bare React Native — Android
The library's AndroidManifest.xml already declares RECORD_AUDIO. You still need to request the permission at runtime — use requestPermission() described below, or handle it yourself before calling start().
Quick start
import { useTuner } from 'react-native-tuner-engine';
export function TunerScreen() {
const { start, stop, latest, isRunning, error } = useTuner({
noiseGateDb: -50,
confidenceThreshold: 0.75,
});
return (
<>
<Text>{latest?.noteName}{latest?.octave} {latest?.cents.toFixed(1)} ¢</Text>
<Button title={isRunning ? 'Stop' : 'Start'} onPress={isRunning ? stop : start} />
</>
);
}useTuner requests the microphone permission, configures the engine, and subscribes to events — all in one call.
API
useTuner(options?)
React hook. Returns { start, stop, latest, isRunning, error }.
import { useTuner } from 'react-native-tuner-engine';
const { start, stop, latest, isRunning, error } = useTuner({
// All fields optional — defaults shown
sampleRate?: number; // 48000
frameSize?: number; // 2048
noiseGateDb?: number; // -70 dBFS
confidenceThreshold?: number; // 0.75 (0–1)
minFrequency?: number; // 60 Hz
maxFrequency?: number; // 1200 Hz
instrument?: Instrument; // 'chromatic'
a4?: number; // 440 Hz
// DSP tuning
emaAlpha?: number; // 0.35 — smoothing (0.05 = slow/stable, 1.0 = instant)
hysteresisFrames?: number; // 3 — frames before a note change is confirmed (1–10)
hpfCutoffHz?: number; // 70 Hz — high-pass filter cutoff (30 for bass, 100 for violin)
});start() — requests mic permission, configures the engine, then begins capture. Throws if permission is denied.
stop() — stops capture. Safe to call when already stopped.
latest — the most recent PitchEvent, or null before the first frame.
error — set if start() throws; null otherwise.
TunerEngine
Low-level imperative API, useful outside of React components.
import { TunerEngine } from 'react-native-tuner-engine';
await TunerEngine.requestPermission(); // → boolean
await TunerEngine.configure({ noiseGateDb: -50 });
await TunerEngine.start();
const unsub = TunerEngine.onPitch((event) => {
if (event.hasPitch) console.log(event.noteName, event.cents);
});
// later:
unsub();
await TunerEngine.stop();Types
type PitchEvent = {
hasPitch: boolean;
frequency: number; // Hz
confidence: number; // 0–1
rmsDb: number; // dBFS
noteName: string; // e.g. "A"
octave: number;
cents: number; // −50 to +50 relative to equal temperament
};
type TunerConfig = {
sampleRate?: number; // default 48000
frameSize?: number; // default 2048
noiseGateDb?: number; // default -70 dBFS
confidenceThreshold?: number; // default 0.75
minFrequency?: number; // default 60 Hz
maxFrequency?: number; // default 1200 Hz
a4?: number; // default 440 Hz
emaAlpha?: number; // default 0.35 — PostProcessor smoothing (0.05–1.0)
hysteresisFrames?: number; // default 3 — frames to confirm a note change (1–10)
hpfCutoffHz?: number; // default 70 — high-pass filter cutoff in Hz (20–300)
onsetDetection?: boolean; // default false — resets smoothing on note attacks
};
type Instrument =
| 'guitar' | 'bass' | 'violin' | 'viola' | 'cello'
| 'ukulele' | 'mandolin' | 'banjo' | 'chromatic';
type Temperament = 'equal' | 'just';Additional methods on TunerEngine
TunerEngine.setA4(hz: number): void // default 440
TunerEngine.setInstrument(name: Instrument): void
TunerEngine.setTemperament(name: Temperament): void
TunerEngine.getStatus(): { isRunning: boolean; engineReady: boolean }For the full API reference (all config options, quality presets, adaptive frame size, overlap ratio), see documents/API.md.
C++ pipeline
The shared C++ core (cpp/) compiles as a static library on both platforms.
| Stage | Class | Notes |
|---|---|---|
| High-pass filter | BiquadHpf | Direct-Form II Transposed, 70 Hz cutoff (configurable), Q 0.707 |
| Windowing | Window | Hann window, precomputed coefficients |
| Pitch detection | DetectorFusion | Fuses a precise primary detector with a coarse corroborator (agree/clash/solo rules within 1 semitone) |
| — primary | PyinPitchDetector | Probabilistic YIN (Beta-distributed threshold prior, per-candidate probability mass) |
| — corroborator | CepstrumPitchDetector | Real cepstrum via radix-2 FFT; peak-vs-rival prominence confidence |
| Note mapping | NoteMapper | Hz → MIDI, note name, octave, cents deviation |
| SNR estimation | SnrEstimator | Signal RMS vs. a noise floor learned from every frame (fast fall, slow rise); full confidence weight from 12 dB SNR |
| Post-processing | PostProcessor | Median-5 filter, EMA smoothing (configurable), note-transition hysteresis (configurable) |
| Dispatch | AudioFrameDispatcher | SPSC lock-free queue, dedicated worker thread |
Performance targets
| Metric | Target | |---|---| | Time to first pitch | < 300 ms | | Per-frame CPU (midrange Android) | < 5 ms | | Per-frame CPU (iPhone 12+) | < 3 ms | | Audio-thread allocations | 0 |
Requirements
- React Native 0.75+ (New Architecture / Bridgeless)
- iOS 13+
- Android API 24+, NDK r26+
