A high-performance React Native Turbo Module for real-time instrument tuning. Native C++ DSP runs on a dedicated worker thread; pitch results stream to JavaScript via JSI (no bridge serialization).
import {
TunerEngine, // Singleton class — imperative API
useTuner, // React hook — declarative API
} from 'react-native-tuner-engine';
// Types
import type {
EngineStatus,
Instrument,
PitchEvent,
QualityPreset,
Temperament,
TunerConfig,
TuningPreset,
} from 'react-native-tuner-engine';import { useTuner } from 'react-native-tuner-engine';
function Tuner() {
const { start, stop, latest, isRunning, error } = useTuner({
instrument: 'guitar',
noiseGateDb: -50,
confidenceThreshold: 0.75,
});
return (
<View>
<Text>{latest?.noteName ?? '—'}</Text>
<Text>{latest?.cents.toFixed(1)} ¢</Text>
<Button title={isRunning ? 'Stop' : 'Start'} onPress={isRunning ? stop : start} />
</View>
);
}The recommended way to integrate the tuner. Manages permissions, lifecycle, and state automatically.
function useTuner(opts?: UseTunerOptions): UseTunerResult;All fields are optional. Extends TunerConfig with:
| Field | Type | Default | Description |
|---|---|---|---|
instrument |
Instrument |
undefined |
Activates the string-matching profile for this instrument |
a4 |
number |
440 |
A4 reference pitch in Hz |
...all TunerConfig fields |
See TunerConfig |
| Field | Type | Description |
|---|---|---|
start |
() => Promise<void> |
Request permission → configure → start listening |
stop |
() => Promise<void> |
Stop audio capture and unsubscribe |
latest |
PitchEvent | null |
Most recent pitch detection result (null before first event) |
isRunning |
boolean |
Whether the engine is actively processing audio |
error |
Error | null |
Last error (permission denied, audio failure, etc.) |
- Calling
start():- Requests microphone permission
- Calls
TunerEngine.configure(config) - Sets instrument and A4 if provided
- Subscribes to pitch events
- Starts native audio capture
- Calling
stop():- Unsubscribes from events
- Stops native audio + DSP pipeline
- On unmount:
- Automatic cleanup (unsubscribe + stop)
Singleton instance for imperative control. Use this when you need fine-grained control beyond what the hook provides.
Configures the DSP pipeline. Must be called before start(). Safe to call while stopped to change settings.
await TunerEngine.configure({
noiseGateDb: -50,
confidenceThreshold: 0.8,
emaAlpha: 0.4,
hpfCutoffHz: 80,
});Starts audio capture and pitch detection. Rejects if the audio session fails to initialize.
await TunerEngine.start();Stops audio capture and the DSP worker thread. Safe to call multiple times.
await TunerEngine.stop();Requests microphone permission from the OS. Returns true if granted, false if denied.
- iOS: Uses
AVCaptureDevice.requestAccess(for: .audio) - Android: Uses
PermissionAwareActivitywithRECORD_AUDIO
const granted = await TunerEngine.requestPermission();
if (!granted) Alert.alert('Microphone access required');Changes the A4 reference frequency. Affects note mapping and cents calculation in real-time (no restart needed).
TunerEngine.setA4(442); // Orchestral tuningActivates a built-in instrument profile. This sets the tuning preset and optimizes frequency range.
TunerEngine.setInstrument('guitar');Sets a specific tuning preset. Pass '' to disable string matching.
TunerEngine.setTuning('guitar_drop_d');Sets the temperament system. 'equal' uses standard 12-TET; 'just' uses 5-limit just intonation ratios (relative to C).
TunerEngine.setTemperament('just');Returns the current engine state synchronously.
const { isRunning, engineReady } = TunerEngine.getStatus();Subscribes to pitch detection events. Returns an unsubscribe function.
- Events fire on every processed audio frame (~23ms at 48kHz / 1024 samples)
- The callback receives a
PitchEventregardless of whether pitch was detected (hasPitchmay befalse)
const unsubscribe = TunerEngine.onPitch((event) => {
if (event.hasPitch) {
console.log(`${event.noteName}${event.octave} — ${event.cents.toFixed(1)}¢`);
}
});
// Later:
unsubscribe();Configuration for the DSP pipeline. All fields optional — defaults are optimized for guitar.
type TunerConfig = {
sampleRate?: number; // Default: 48000. Audio sample rate in Hz.
frameSize?: number; // Default: 2048. DSP frame size in samples.
noiseGateDb?: number; // Default: -55. Frames quieter than this (dBFS) are ignored.
confidenceThreshold?: number; // Default: 0.75. Minimum confidence (0–1) to report pitch.
minFrequency?: number; // Default: 60. Lowest detectable frequency in Hz.
maxFrequency?: number; // Default: 1200. Highest detectable frequency in Hz.
a4?: number; // Default: 440. A4 reference pitch in Hz.
emaAlpha?: number; // Default: 0.35. EMA smoothing (0.05–1.0). Lower = smoother.
hysteresisFrames?: number; // Default: 3. Frames needed to confirm note change (1–10).
hpfCutoffHz?: number; // Default: 70. High-pass filter cutoff in Hz (20–300).
onsetDetection?: boolean; // Default: false. Resets PostProcessor on note attacks for faster response.
overlapRatio?: number; // Default: 0. Frame overlap (0.0–0.75). Higher = more updates, more CPU.
adaptiveFrameSize?: boolean; // Default: true. Auto-select frame size per instrument preset.
quality?: QualityPreset; // Overrides frameSize + overlapRatio with a named preset.
};Overlap & Adaptive Frame Size:
The overlapRatio controls how much of the analysis frame overlaps with the previous frame. With 75% overlap and 2048 frame size, the engine produces pitch updates every ~10.7ms instead of every ~42.7ms.
When adaptiveFrameSize is true (default), setting the instrument to 'bass' or 'cello' automatically switches to a 4096-sample frame for better low-frequency resolution (min detectable drops from 46.9 Hz to 23.4 Hz at 48 kHz). Explicitly setting frameSize overrides this behavior.
The quality preset is a shorthand:
| Preset | Frame Size | Overlap | Update Rate | Best For |
|---|---|---|---|---|
'low-latency' |
1024 | 0% | ~21 ms | Real-time feedback, high strings |
'balanced' |
2048 | 50% | ~21 ms | General use |
'high-accuracy' |
4096 | 75% | ~21 ms | Bass guitar, cello |
Emitted on every processed audio frame.
type PitchEvent = {
hasPitch: boolean; // true if a valid pitch was detected this frame
frequency: number; // Detected fundamental frequency in Hz (0 if no pitch)
confidence: number; // Detection confidence 0.0–1.0
rmsDb: number; // Frame RMS level in dBFS (e.g. -30 = moderate volume)
noteName: string; // Nearest note name: "C", "C#", "D", ... "B"
octave: number; // MIDI octave number (e.g. 4 for A4=440Hz)
cents: number; // Deviation from nearest note in cents (-50 to +50)
nearestString: string; // Nearest string name (e.g. "E2") or "" if no tuning active
stringDeviation: number; // Cents from that string's target (negative = flat)
};Note on hasPitch: false frames:
When the signal is below the noise gate or confidence is too low, hasPitch is false. All other fields will be zero/empty. Use this to show "listening…" states in your UI.
type Instrument =
| 'guitar' // Standard 6-string (E2–E4)
| 'bass' // 4-string bass (E1–G3)
| 'violin' // (G3–E6)
| 'viola' // (C3–A5)
| 'cello' // (C2–A4)
| 'ukulele' // (G4–A4)
| 'mandolin' // (G3–E5)
| 'banjo' // 5-string (D3–D4)
| 'chromatic'; // No string matching, full rangetype TuningPreset =
| 'guitar_standard' // E2 A2 D3 G3 B3 E4
| 'guitar_eb_standard' // Eb2 Ab2 Db3 Gb3 Bb3 Eb4
| 'guitar_d_standard' // D2 G2 C3 F3 A3 D4
| 'guitar_drop_d' // D2 A2 D3 G3 B3 E4
| 'guitar_drop_c' // C2 G2 C3 F3 A3 D4
| 'guitar_open_g' // D2 G2 D3 G3 B3 D4
| 'guitar_open_d' // D2 A2 D3 F#3 A3 D4
| 'guitar_open_c' // C2 G2 C3 G3 C4 E4
| 'guitar_dadgad' // D2 A2 D3 G3 A3 D4
| 'bass_standard' // E1 A1 D2 G2
| 'bass_drop_d' // D1 A1 D2 G2
| 'violin_standard' // G3 D4 A4 E5
| 'viola_standard' // C3 G3 D4 A4
| 'cello_standard' // C2 G2 D3 A3
| 'ukulele_standard'; // G4 C4 E4 A4type Temperament = 'equal' | 'just';'equal'— Standard 12-tone equal temperament (default).'just'— 5-limit just intonation. Target frequencies are shifted by the pure-ratio cent offsets relative to C, so the tuner shows deviation from just intervals rather than equal-tempered ones.
type QualityPreset = 'low-latency' | 'balanced' | 'high-accuracy';A convenience type that configures frameSize and overlapRatio together. See TunerConfig for the mapping table.
type EngineStatus = {
isRunning: boolean; // Audio capture is active
engineReady: boolean; // Native DSP pipeline is initialized
};┌──────────────────────────────────────────────────────────────────────────┐
│ NATIVE (C++ worker thread) │
│ │
│ Microphone (iOS: AVAudioEngine / Android: Oboe) │
│ │ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ LockFreeRingBuffer. │ ◄── Audio thread pushes PCM float samples │
│ └──────────┬──────────┘ │
│ │ Worker thread pops fixed-size frames │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ High-Pass Filter │ Biquad HPF (removes DC + rumble) │
│ └──────────┬──────────┘ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ Noise Gate (RMS) │ Below threshold → emit hasPitch=false │
│ └──────────┬──────────┘ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ Onset Detector │ Energy rise → resets PostProcessor (optional) │
│ └──────────┬──────────┘ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ Hann Window │ Spectral leakage reduction │
│ └──────────┬──────────┘ │
│ ▼ │
│ ┌─────────────────────────────────────────┐ │
│ │ Pitch Detectors (parallel) │ │
│ │ • YIN (time-domain CMND) │ │
│ │ • PYIN (probabilistic + alias │ │
│ │ pruning) │ │
│ │ • Cepstrum (frequency-domain) │ │
│ └──────────┬──────────────────────────────┘ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ Ensemble Selector │ Picks most consistent result across detectors │
│ └──────────┬──────────┘ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ PostProcessor │ Median-5 → EMA smooth → hysteresis │
│ └──────────┬──────────┘ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ Note Mapper │ freq → MIDI note → name + octave + cents │
│ └──────────┬──────────┘ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ String Matcher │ freq → nearest string + deviation (optional) │
│ └──────────┬──────────┘ │
│ │ │
└─────────────┼────────────────────────────────────────────────────────────┘
│
│ PitchResult struct
▼
┌──────────────────────────────────────────────────────────────────────────┐
│ BRIDGE │
│ │
│ iOS: jsInvoker->invokeAsync() → sets JSI object properties │
│ Android: JNI CallVoidMethod → RCTDeviceEventEmitter.emit("onPitch") │
│ │
└──────────────┬───────────────────────────────────────────────────────────┘
│
│ PitchEvent (JS object)
▼
┌──────────────────────────────────────────────────────────────────────────┐
│ JAVASCRIPT │
│ │
│ • JSI path: globalThis.__tunerEngineOnPitch(event) ← zero-copy │
│ • Fallback: DeviceEventEmitter "onPitch" listener │
│ │
│ useTuner hook: latest state updates → React re-render │
│ │
└──────────────────────────────────────────────────────────────────────────┘
| Stage | Typical Duration |
|---|---|
| Audio buffer fill (1024 samples @ 48kHz) | ~21 ms |
| HPF + Window + Detectors | ~1–3 ms |
| PostProcessor + NoteMapper | < 0.1 ms |
| JSI invoke (iOS) / JNI callback (Android) | < 0.5 ms |
| Total glass-to-glass | ~23–25 ms |
useTuner({
instrument: 'guitar',
noiseGateDb: -50,
confidenceThreshold: 0.75,
hpfCutoffHz: 70,
emaAlpha: 0.35,
hysteresisFrames: 3,
});useTuner({
instrument: 'bass',
noiseGateDb: -55,
confidenceThreshold: 0.7,
hpfCutoffHz: 30, // Lower cutoff to preserve fundamental
minFrequency: 25, // B0 = 30.87 Hz
emaAlpha: 0.25, // Smoother — bass notes ring longer
hysteresisFrames: 5,
});useTuner({
instrument: 'violin',
noiseGateDb: -45,
confidenceThreshold: 0.8,
hpfCutoffHz: 150, // Cut all rumble below G3
minFrequency: 180,
maxFrequency: 2000,
emaAlpha: 0.5, // Faster response for bowed strings
hysteresisFrames: 2,
});useTuner({
instrument: 'chromatic',
onsetDetection: true, // Instant response on attacks
emaAlpha: 1.0, // No smoothing
hysteresisFrames: 1, // Instant note switching
confidenceThreshold: 0.6,
});useTuner({
instrument: 'guitar',
emaAlpha: 0.15, // Heavy smoothing
hysteresisFrames: 8, // Very stable display
confidenceThreshold: 0.85,
noiseGateDb: -40, // Ignore quiet signals
});| Parameter | Lower Value | Higher Value |
|---|---|---|
noiseGateDb |
More sensitive to quiet signals | Ignores background noise better |
confidenceThreshold |
Detects uncertain pitches (may flicker) | Only shows confident detections |
emaAlpha |
Smoother, slower response | Jittery but instant |
hysteresisFrames |
Fast note switching (may bounce) | Stable display (slight delay) |
hpfCutoffHz |
Preserves low fundamentals | Removes more rumble/handling noise |
onsetDetection |
— (disabled) | Resets smoothing on note attacks → faster note switching |
Use the rmsDb field from PitchEvent:
- Start the tuner in a quiet room
- Note the
rmsDbvalues when silent (e.g. -65 dB) - Play your instrument softly, note the
rmsDb(e.g. -35 dB) - Set
noiseGateDbhalfway between (e.g. -50 dB)