Native audio analysis for React Native via Nitro Modules. Decodes WAV files on-device, extracts DSP features (RMS, transient density, windowing), detects content heuristics, scores prominence, and selects highlight segments — all without leaving the JS thread blocked.
- WAV decoding — iOS via AVFoundation; Android via a built-in 16-bit PCM parser
- Overlapping window analysis — 1.5 s windows with 0.75 s hop
- Transient density — per-window sharp energy-change detection (transients/s)
- Event detection — heuristic labels:
speech,music,impact,ambient,silence - Prominence scoring — weighted score in
[0, 1]with quality penalties - Highlight selection — up to 3 non-overlapping peak windows
- Synchronous version info —
getVersion()andgetPlatformInfo()run on the native thread
- React Native ≥ 0.76 (tested on 0.85)
react-native-nitro-modules≥ 0.35.2
npm install react-native-audio-intelligence react-native-nitro-modules
# or
yarn add react-native-audio-intelligence react-native-nitro-modulesThen rebuild the native app:
npx pod-install # iOS
npx react-native run-ios
npx react-native run-androidimport AudioIntelligence from 'react-native-audio-intelligence'
const version = AudioIntelligence.getVersion()
// => "1.0.0"
const result = await AudioIntelligence.analyzeAudioFile('/path/to/audio.wav')
console.log(result.score) // 0.0 – 1.0 prominence
console.log(result.detectedEvents) // [{ label: "speech", confidence: 0.47 }, ...]
console.log(result.windows.length) // overlapping analysis windowsThe file path must be an absolute path readable by native code. React Native asset bundles are not supported directly — copy or record to a temp path first.
| Platform | Example path | Notes |
|---|---|---|
| iOS | /tmp/real_test.wav |
Simulator/device temp directory |
| Android | /data/local/tmp/test.wav |
Push with adb push audio.wav /data/local/tmp/ |
# Push a test file to an Android device/emulator
adb push ./fixtures/test.wav /data/local/tmp/test_transient.wavReturns the native module version synchronously.
Returns the host OS name and version.
{ platform: 'ios' | 'android', version: string }Analyzes a WAV file at the given absolute path. Resolves with a full result object; rejects if the file is missing, unreadable, or not a supported format.
Supported formats
- iOS: any format AVFoundation can decode (WAV, AAC, MP3, etc.)
- Android: 16-bit PCM WAV only
| Field | Type | Description |
|---|---|---|
score |
number |
Overall prominence score, clamped to [0, 1] |
tags |
string[] |
Top-level tags (e.g. ["voice"], ["silence"]) |
detectedEvents |
DetectedEvent[] |
Heuristic content detections, sorted by confidence |
highlights |
Highlight[] |
Up to 3 peak windows suitable for preview clips |
quality |
AudioQuality |
Clipping, silence, and wind-noise flags |
windows |
AudioWindow[] |
Overlapping per-window DSP metrics |
| Field | Type | Description |
|---|---|---|
startMs |
number |
Window start offset in milliseconds |
durationMs |
number |
Window length in milliseconds |
rms |
number |
Root-mean-square energy [0, 1] |
transientDensity |
number |
Sharp energy rises per second |
{
label: string
confidence: number
} // confidence in [0, 1]Labels: speech, music, impact, ambient, silence.
{ startMs: number; durationMs: number; score: number; tags: string[] }{
clipping: boolean
windNoise: boolean
silence: boolean
}A minimal screen that runs analysis on launch and renders the results inline. Adjust TEST_WAV_PATH for your platform.
import React, { useCallback, useEffect, useState } from 'react'
import {
ActivityIndicator,
Platform,
ScrollView,
StyleSheet,
Text,
View,
} from 'react-native'
import AudioIntelligence, {
type AudioAnalysisResult,
} from 'react-native-audio-intelligence'
const TEST_WAV_PATH =
Platform.OS === 'ios'
? '/tmp/real_test.wav'
: '/data/local/tmp/test_transient.wav'
const LOG = '[AudioIntelligence]'
function logAnalysis(result: AudioAnalysisResult, platform: string) {
console.log(`${LOG} ── Analysis complete (${platform}) ──`)
console.log(`${LOG} Prominence score: ${result.score.toFixed(5)}`)
console.log(`${LOG} Tags: ${result.tags.join(', ') || '—'}`)
console.log(
`${LOG} Quality: clipping=${result.quality.clipping} ` +
`silence=${result.quality.silence} wind=${result.quality.windNoise}`
)
console.log(`${LOG} Windows (${result.windows.length}):`)
result.windows.forEach((w, i) => {
console.log(
`${LOG} [${i}] ${w.startMs.toFixed(0)}ms–` +
`${(w.startMs + w.durationMs).toFixed(0)}ms ` +
`rms=${w.rms.toFixed(4)} transients=${w.transientDensity.toFixed(2)}/s`
)
})
if (result.highlights.length > 0) {
console.log(`${LOG} Highlights (${result.highlights.length}):`)
result.highlights.forEach((h, i) => {
console.log(
`${LOG} [${i}] ${h.startMs.toFixed(0)}ms score=${h.score.toFixed(3)}`
)
})
}
console.log(`${LOG} Events (${result.detectedEvents.length}):`)
result.detectedEvents.forEach((e, i) => {
console.log(
`${LOG} [${i}] ${e.label} (${(e.confidence * 100).toFixed(1)}%)`
)
})
}
function formatPercent(value: number) {
return `${(value * 100).toFixed(1)}%`
}
export default function App() {
const [status, setStatus] = useState<'idle' | 'loading' | 'done' | 'error'>(
'idle'
)
const [error, setError] = useState<string | null>(null)
const [result, setResult] = useState<AudioAnalysisResult | null>(null)
const [version, setVersion] = useState<string | null>(null)
const runAnalysis = useCallback(async () => {
setStatus('loading')
setError(null)
setResult(null)
try {
const v = AudioIntelligence.getVersion()
setVersion(v)
console.log(`${LOG} Module v${v} · ${Platform.OS}`)
console.log(`${LOG} File: ${TEST_WAV_PATH}`)
const analysis = await AudioIntelligence.analyzeAudioFile(TEST_WAV_PATH)
logAnalysis(analysis, Platform.OS)
setResult(analysis)
setStatus('done')
} catch (e) {
const message = e instanceof Error ? e.message : String(e)
console.error(`${LOG} Analysis failed:`, message)
setError(message)
setStatus('error')
}
}, [])
useEffect(() => {
runAnalysis()
}, [runAnalysis])
return (
<View style={styles.root}>
<View style={styles.header}>
<Text style={styles.title}>Audio Intelligence</Text>
{version && <Text style={styles.subtitle}>v{version}</Text>}
</View>
{status === 'loading' && (
<View style={styles.centered}>
<ActivityIndicator size="large" color="#6C8EFF" />
<Text style={styles.muted}>Analyzing audio…</Text>
</View>
)}
{status === 'error' && (
<View style={styles.card}>
<Text style={styles.errorLabel}>Analysis failed</Text>
<Text style={styles.errorText}>{error}</Text>
</View>
)}
{result && (
<ScrollView
style={styles.scroll}
contentContainerStyle={styles.scrollContent}
>
<View style={styles.scoreCard}>
<Text style={styles.scoreLabel}>Prominence</Text>
<Text style={styles.scoreValue}>{formatPercent(result.score)}</Text>
<Text style={styles.tags}>{result.tags.join(' · ')}</Text>
</View>
<Section title={`Events (${result.detectedEvents.length})`}>
{result.detectedEvents.map((e, i) => (
<Row
key={`${e.label}-${i}`}
label={e.label}
value={formatPercent(e.confidence)}
/>
))}
</Section>
<Section title={`Windows (${result.windows.length})`}>
{result.windows.map((w, i) => (
<Row
key={i}
label={`${w.startMs.toFixed(0)}ms`}
value={`rms ${w.rms.toFixed(3)} · ${w.transientDensity.toFixed(1)}/s`}
/>
))}
</Section>
{result.highlights.length > 0 && (
<Section title={`Highlights (${result.highlights.length})`}>
{result.highlights.map((h, i) => (
<Row
key={i}
label={`${h.startMs.toFixed(0)}ms`}
value={formatPercent(h.score)}
/>
))}
</Section>
)}
<Section title="Quality">
<Row
label="Clipping"
value={result.quality.clipping ? 'yes' : 'no'}
/>
<Row
label="Silence"
value={result.quality.silence ? 'yes' : 'no'}
/>
<Row
label="Wind noise"
value={result.quality.windNoise ? 'yes' : 'no'}
/>
</Section>
</ScrollView>
)}
</View>
)
}
function Section({
title,
children,
}: {
title: string
children: React.ReactNode
}) {
return (
<View style={styles.section}>
<Text style={styles.sectionTitle}>{title}</Text>
<View style={styles.sectionBody}>{children}</View>
</View>
)
}
function Row({ label, value }: { label: string; value: string }) {
return (
<View style={styles.row}>
<Text style={styles.rowLabel}>{label}</Text>
<Text style={styles.rowValue}>{value}</Text>
</View>
)
}
const styles = StyleSheet.create({
root: {
flex: 1,
backgroundColor: '#0D0F14',
},
header: {
paddingTop: 60,
paddingBottom: 20,
paddingHorizontal: 24,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: '#1E2230',
},
title: {
fontSize: 28,
fontWeight: '700',
color: '#F0F2F8',
letterSpacing: -0.5,
},
subtitle: {
marginTop: 4,
fontSize: 14,
color: '#6C8EFF',
fontWeight: '500',
},
centered: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
gap: 12,
},
muted: {
color: '#6B7280',
fontSize: 15,
},
scroll: {
flex: 1,
},
scrollContent: {
padding: 20,
gap: 16,
paddingBottom: 40,
},
scoreCard: {
backgroundColor: '#161A24',
borderRadius: 16,
padding: 24,
alignItems: 'center',
borderWidth: 1,
borderColor: '#1E2230',
},
scoreLabel: {
fontSize: 13,
fontWeight: '600',
color: '#6B7280',
textTransform: 'uppercase',
letterSpacing: 1,
},
scoreValue: {
fontSize: 48,
fontWeight: '700',
color: '#6C8EFF',
marginTop: 4,
},
tags: {
marginTop: 8,
fontSize: 14,
color: '#9CA3AF',
},
section: {
backgroundColor: '#161A24',
borderRadius: 12,
overflow: 'hidden',
borderWidth: 1,
borderColor: '#1E2230',
},
sectionTitle: {
fontSize: 13,
fontWeight: '600',
color: '#6B7280',
textTransform: 'uppercase',
letterSpacing: 0.8,
paddingHorizontal: 16,
paddingTop: 14,
paddingBottom: 8,
},
sectionBody: {
paddingBottom: 4,
},
row: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: 16,
paddingVertical: 11,
borderTopWidth: StyleSheet.hairlineWidth,
borderTopColor: '#1E2230',
},
rowLabel: {
fontSize: 15,
color: '#D1D5DB',
textTransform: 'capitalize',
},
rowValue: {
fontSize: 15,
color: '#9CA3AF',
fontVariant: ['tabular-nums'],
},
card: {
margin: 20,
backgroundColor: '#1C1014',
borderRadius: 12,
padding: 20,
borderWidth: 1,
borderColor: '#3B1C24',
},
errorLabel: {
fontSize: 16,
fontWeight: '600',
color: '#F87171',
marginBottom: 8,
},
errorText: {
fontSize: 14,
color: '#FCA5A5',
lineHeight: 20,
},
})[AudioIntelligence] Module v0.3.0 · android
[AudioIntelligence] File: /data/local/tmp/test_transient.wav
[AudioIntelligence] ── Analysis complete (android) ──
[AudioIntelligence] Prominence score: 0.38922
[AudioIntelligence] Tags: voice
[AudioIntelligence] Quality: clipping=false silence=false wind=false
[AudioIntelligence] Windows (4):
[AudioIntelligence] [0] 0ms–1500ms rms=0.0413 transients=0.67/s
[AudioIntelligence] [1] 750ms–2250ms rms=0.0598 transients=1.33/s
[AudioIntelligence] [2] 1500ms–3000ms rms=0.0890 transients=1.33/s
[AudioIntelligence] [3] 2250ms–3000ms rms=0.1099 transients=1.33/s
[AudioIntelligence] Events (1):
[AudioIntelligence] [0] speech (47.0%)
The prominence score combines five weighted components:
| Component | Weight | Source |
|---|---|---|
| Volume | 0.35 | Whole-file RMS |
| Transients | 0.25 | Average transient density across windows |
| Frequency richness | 0.20 | RMS variance across windows |
| Voice presence | 0.10 | speech event confidence |
| Contrast | 0.10 | Peak-to-floor RMS ratio across windows |
Penalties are applied for clipping (−0.15), silence (−0.50), and wind noise (−0.10). The final value is clamped to [0, 1].
Event detection is currently DSP-heuristic (not ML). ONNX model integration is planned for a future release.
analyzeAudioFile rejects when:
- The file does not exist at the given path
- The file cannot be decoded (unsupported format, corrupt data)
- On Android: the file is not 16-bit PCM WAV
Always use a try/catch (or .catch()) around the promise and surface the error message to the user.
MIT