react-native-simple-note-pitch-detector 0.6.1 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/src/main/java/expo/modules/simplenotepitchdetector/PitchAnalyzer.kt +227 -79
- package/android/src/main/java/expo/modules/simplenotepitchdetector/ReactNativeSimpleNotePitchDetectorModule.kt +8 -0
- package/build/ReactNativeSimpleNotePitchDetector.types.d.ts +0 -3
- package/build/ReactNativeSimpleNotePitchDetector.types.d.ts.map +1 -1
- package/build/ReactNativeSimpleNotePitchDetector.types.js.map +1 -1
- package/build/index.d.ts +5 -5
- package/build/index.d.ts.map +1 -1
- package/build/index.js +2 -5
- package/build/index.js.map +1 -1
- package/package.json +6 -4
- package/src/ReactNativeSimpleNotePitchDetector.types.ts +0 -4
- package/src/index.ts +7 -17
- package/android/.gradle/9.0-milestone-1/checksums/checksums.lock +0 -0
- package/android/.gradle/9.0-milestone-1/fileChanges/last-build.bin +0 -0
- package/android/.gradle/9.0-milestone-1/fileHashes/fileHashes.lock +0 -0
- package/android/.gradle/9.0-milestone-1/gc.properties +0 -0
- package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
- package/android/.gradle/buildOutputCleanup/cache.properties +0 -2
- package/android/.gradle/config.properties +0 -2
- package/android/.gradle/vcs-1/gc.properties +0 -0
- package/android/.idea/AndroidProjectSystem.xml +0 -6
- package/android/.idea/caches/deviceStreaming.xml +0 -1318
- package/android/.idea/gradle.xml +0 -13
- package/android/.idea/migrations.xml +0 -10
- package/android/.idea/misc.xml +0 -9
- package/android/.idea/runConfigurations.xml +0 -17
- package/android/.idea/vcs.xml +0 -6
- package/android/local.properties +0 -8
- package/build/ReactNativeSimpleNotePitchDetector.web.d.ts +0 -4
- package/build/ReactNativeSimpleNotePitchDetector.web.d.ts.map +0 -1
- package/build/ReactNativeSimpleNotePitchDetector.web.js +0 -7
- package/build/ReactNativeSimpleNotePitchDetector.web.js.map +0 -1
- package/build/ReactNativeSimpleNotePitchDetectorView.d.ts +0 -4
- package/build/ReactNativeSimpleNotePitchDetectorView.d.ts.map +0 -1
- package/build/ReactNativeSimpleNotePitchDetectorView.js +0 -7
- package/build/ReactNativeSimpleNotePitchDetectorView.js.map +0 -1
- package/src/ReactNativeSimpleNotePitchDetector.web.tsx +0 -13
- package/src/ReactNativeSimpleNotePitchDetectorView.tsx +0 -11
|
@@ -2,15 +2,19 @@ package expo.modules.simplepitchdetector
|
|
|
2
2
|
|
|
3
3
|
import android.util.Log
|
|
4
4
|
import be.tarsos.dsp.AudioDispatcher
|
|
5
|
+
import be.tarsos.dsp.AudioEvent
|
|
5
6
|
import be.tarsos.dsp.AudioProcessor
|
|
6
7
|
import be.tarsos.dsp.io.android.AudioDispatcherFactory
|
|
7
8
|
import be.tarsos.dsp.pitch.PitchDetectionHandler
|
|
9
|
+
import be.tarsos.dsp.pitch.PitchDetectionResult
|
|
8
10
|
import be.tarsos.dsp.pitch.PitchProcessor
|
|
9
11
|
import be.tarsos.dsp.pitch.PitchProcessor.PitchEstimationAlgorithm
|
|
12
|
+
import be.tarsos.dsp.util.fft.FFT
|
|
10
13
|
import kotlin.math.log2
|
|
11
14
|
import kotlin.math.round
|
|
12
|
-
import kotlin.math.
|
|
13
|
-
import kotlin.math.
|
|
15
|
+
import kotlin.math.ln
|
|
16
|
+
import kotlin.math.cos
|
|
17
|
+
import kotlin.math.PI
|
|
14
18
|
|
|
15
19
|
private const val TAG = "PitchAnalyzer"
|
|
16
20
|
|
|
@@ -22,6 +26,14 @@ data class PitchData(
|
|
|
22
26
|
val offset: Float
|
|
23
27
|
)
|
|
24
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Dual-dispatcher pitch detector:
|
|
31
|
+
* - HPS (Harmonic Product Spectrum, h=2,3,4,5) on its own AudioDispatcher for low notes (<200Hz)
|
|
32
|
+
* - MPM (McLeod Pitch Method) on a separate AudioDispatcher for mid/high notes (>=200Hz)
|
|
33
|
+
*
|
|
34
|
+
* Two completely separate microphone streams to avoid any shared state or buffer corruption.
|
|
35
|
+
* Previous experiments proved that ANY combination of HPS+MPM in the same dispatcher degrades HPS.
|
|
36
|
+
*/
|
|
25
37
|
class PitchAnalyzer {
|
|
26
38
|
|
|
27
39
|
private val notes = arrayOf("C","C#","D","D#","E","F","F#","G","G#","A","A#","B")
|
|
@@ -30,74 +42,200 @@ class PitchAnalyzer {
|
|
|
30
42
|
private var onStatus: ((String, String) -> Unit)? = null
|
|
31
43
|
private var isRecording = false
|
|
32
44
|
private var levelThreshold = -30f
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
//
|
|
38
|
-
private var
|
|
39
|
-
private var
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
private var
|
|
43
|
-
private var
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
45
|
+
private var sampleRate = 44100
|
|
46
|
+
private var bufferSize = 8192
|
|
47
|
+
private var algorithmName = "hps"
|
|
48
|
+
|
|
49
|
+
// HPS dispatcher (low notes)
|
|
50
|
+
private var hpsDispatcher: AudioDispatcher? = null
|
|
51
|
+
private var hpsThread: Thread? = null
|
|
52
|
+
|
|
53
|
+
// MPM dispatcher (mid/high notes)
|
|
54
|
+
private var mpmDispatcher: AudioDispatcher? = null
|
|
55
|
+
private var mpmThread: Thread? = null
|
|
56
|
+
|
|
57
|
+
// HPS pre-allocated buffers
|
|
58
|
+
private var fftSize = 0
|
|
59
|
+
private var freqResolution = 0f
|
|
60
|
+
private var hpsFft: FFT? = null
|
|
61
|
+
private var fftBuffer: FloatArray? = null
|
|
62
|
+
private var magnitudes: FloatArray? = null
|
|
63
|
+
private var hpsProduct: FloatArray? = null
|
|
64
|
+
private var hannWindow: FloatArray? = null
|
|
65
|
+
|
|
66
|
+
// HPS searches low range only: A0 (27.5Hz) to ~G3 (196Hz)
|
|
67
|
+
private val hpsMinHz = 26f
|
|
68
|
+
private val hpsMaxHz = 200f
|
|
69
|
+
|
|
70
|
+
// MPM crossover: only emit MPM results >= this frequency
|
|
71
|
+
private val mpmMinHz = 200f
|
|
72
|
+
// MPM max: reject garbage above piano range (C8 = 4186Hz)
|
|
73
|
+
private val mpmMaxHz = 4200f
|
|
74
|
+
|
|
75
|
+
private val hpsProcessor = object : AudioProcessor {
|
|
76
|
+
override fun process(audioEvent: AudioEvent): Boolean {
|
|
77
|
+
val buffer = audioEvent.floatBuffer
|
|
78
|
+
val dB = audioEvent.getdBSPL().toFloat()
|
|
79
|
+
|
|
80
|
+
if (dB > levelThreshold) {
|
|
81
|
+
try {
|
|
82
|
+
val pitch = detectPitchHPS(buffer)
|
|
83
|
+
if (pitch > 0f) {
|
|
84
|
+
emitPitch(pitch, dB)
|
|
85
|
+
}
|
|
86
|
+
} catch (ex: Exception) {
|
|
87
|
+
Log.e(TAG, "HPS failed: ${ex.message}")
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return true
|
|
54
91
|
}
|
|
92
|
+
|
|
93
|
+
override fun processingFinished() {}
|
|
55
94
|
}
|
|
56
95
|
|
|
57
|
-
private fun
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
96
|
+
private fun detectPitchHPS(buffer: FloatArray): Float {
|
|
97
|
+
val fft = hpsFft ?: return -1f
|
|
98
|
+
val fftBuf = fftBuffer ?: return -1f
|
|
99
|
+
val mags = magnitudes ?: return -1f
|
|
100
|
+
val hps = hpsProduct ?: return -1f
|
|
101
|
+
val window = hannWindow ?: return -1f
|
|
102
|
+
|
|
103
|
+
// Window and zero-pad
|
|
104
|
+
for (i in buffer.indices) {
|
|
105
|
+
fftBuf[i] = buffer[i] * window[i]
|
|
106
|
+
}
|
|
107
|
+
for (i in buffer.size until fftSize) {
|
|
108
|
+
fftBuf[i] = 0f
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// FFT
|
|
112
|
+
fft.forwardTransform(fftBuf)
|
|
113
|
+
fft.modulus(fftBuf, mags)
|
|
114
|
+
|
|
115
|
+
// HPS: multiply magnitudes at harmonics 2, 3, 4, 5
|
|
116
|
+
val hpsSize = mags.size / 5
|
|
117
|
+
for (i in 0 until hpsSize) {
|
|
118
|
+
hps[i] = mags[2 * i] * mags[3 * i] * mags[4 * i] * mags[5 * i]
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Search for peak in low range only
|
|
122
|
+
val minBin = (hpsMinHz / freqResolution).toInt().coerceAtLeast(1)
|
|
123
|
+
val maxBin = (hpsMaxHz / freqResolution).toInt().coerceAtMost(hpsSize - 1)
|
|
124
|
+
|
|
125
|
+
var peakBin = minBin
|
|
126
|
+
var peakVal = hps[minBin]
|
|
127
|
+
for (i in minBin + 1..maxBin) {
|
|
128
|
+
if (hps[i] > peakVal) { peakBin = i; peakVal = hps[i] }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// SNR check
|
|
132
|
+
val snrStart = (peakBin - 200).coerceAtLeast(minBin)
|
|
133
|
+
val snrEnd = (peakBin + 200).coerceAtMost(maxBin)
|
|
134
|
+
var sum = 0.0
|
|
135
|
+
for (i in snrStart..snrEnd) { sum += hps[i].toDouble() }
|
|
136
|
+
val mean = sum / (snrEnd - snrStart + 1)
|
|
137
|
+
val snr = if (mean > 0.0) peakVal.toDouble() / mean else 0.0
|
|
138
|
+
if (snr < 12.0) return -1f
|
|
139
|
+
|
|
140
|
+
return interpolatePeak(hps, peakBin, minBin, maxBin) * freqResolution
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private fun interpolatePeak(spectrum: FloatArray, peakBin: Int, minBin: Int, maxBin: Int): Float {
|
|
144
|
+
if (peakBin <= minBin || peakBin >= maxBin) return peakBin.toFloat()
|
|
145
|
+
val prev = spectrum[peakBin - 1].coerceAtLeast(1e-30f)
|
|
146
|
+
val curr = spectrum[peakBin].coerceAtLeast(1e-30f)
|
|
147
|
+
val next = spectrum[peakBin + 1].coerceAtLeast(1e-30f)
|
|
148
|
+
val logPrev = ln(prev)
|
|
149
|
+
val logCurr = ln(curr)
|
|
150
|
+
val logNext = ln(next)
|
|
151
|
+
val denom = logPrev - 2f * logCurr + logNext
|
|
152
|
+
return if (denom != 0f) {
|
|
153
|
+
peakBin + 0.5f * (logPrev - logNext) / denom
|
|
154
|
+
} else {
|
|
155
|
+
peakBin.toFloat()
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private fun prepareHPS() {
|
|
160
|
+
val overlap = (bufferSize * 3) / 4
|
|
161
|
+
logStatus("debug", "prepareHPS() sampleRate=$sampleRate, bufferSize=$bufferSize, overlap=$overlap")
|
|
162
|
+
|
|
163
|
+
// 4x zero-padding for frequency resolution
|
|
164
|
+
fftSize = bufferSize * 4
|
|
165
|
+
freqResolution = sampleRate.toFloat() / fftSize
|
|
166
|
+
hpsFft = FFT(fftSize)
|
|
167
|
+
fftBuffer = FloatArray(fftSize)
|
|
168
|
+
magnitudes = FloatArray(fftSize / 2)
|
|
169
|
+
hpsProduct = FloatArray(fftSize / (2 * 5))
|
|
170
|
+
hannWindow = FloatArray(bufferSize) { i ->
|
|
171
|
+
(0.5 * (1.0 - cos(2.0 * PI * i / (bufferSize - 1)))).toFloat()
|
|
69
172
|
}
|
|
173
|
+
|
|
174
|
+
val maxHpsHz = (fftSize / (2 * 5)) * freqResolution
|
|
175
|
+
logStatus("debug", "HPS: fftSize=$fftSize, freqRes=${freqResolution}Hz/bin, search=${hpsMinHz}-${hpsMaxHz}Hz, maxHpsHz=$maxHpsHz")
|
|
176
|
+
|
|
177
|
+
hpsDispatcher = AudioDispatcherFactory.fromDefaultMicrophone(sampleRate, bufferSize, overlap)
|
|
178
|
+
hpsDispatcher?.addAudioProcessor(hpsProcessor)
|
|
179
|
+
|
|
180
|
+
logStatus("debug", "HPS dispatcher prepared (low notes <${hpsMaxHz}Hz)")
|
|
70
181
|
}
|
|
71
182
|
|
|
72
|
-
private fun
|
|
73
|
-
|
|
74
|
-
|
|
183
|
+
private fun prepareMPM() {
|
|
184
|
+
val mpmBufferSize = 2048
|
|
185
|
+
val mpmOverlap = (mpmBufferSize * 3) / 4
|
|
186
|
+
logStatus("debug", "prepareMPM() sampleRate=$sampleRate, bufferSize=$mpmBufferSize, overlap=$mpmOverlap")
|
|
187
|
+
|
|
188
|
+
val pitchHandler = PitchDetectionHandler { result: PitchDetectionResult, event: AudioEvent ->
|
|
189
|
+
val pitch = result.pitch
|
|
190
|
+
if (pitch > 0 && pitch >= mpmMinHz && pitch <= mpmMaxHz && result.isPitched) {
|
|
191
|
+
val dB = event.getdBSPL().toFloat()
|
|
192
|
+
emitPitch(pitch, dB)
|
|
193
|
+
}
|
|
75
194
|
}
|
|
76
195
|
|
|
77
|
-
//
|
|
196
|
+
// Use YIN instead of MPM — McLeodPitchMethod.peakPicking throws AssertionError
|
|
197
|
+
// when two AudioRecord instances compete for the microphone
|
|
198
|
+
val yinPitchProcessor = PitchProcessor(PitchEstimationAlgorithm.YIN, sampleRate.toFloat(), mpmBufferSize, pitchHandler)
|
|
199
|
+
val safeYinProcessor = object : AudioProcessor {
|
|
200
|
+
override fun process(audioEvent: AudioEvent): Boolean {
|
|
201
|
+
try {
|
|
202
|
+
return yinPitchProcessor.process(audioEvent)
|
|
203
|
+
} catch (e: Exception) {
|
|
204
|
+
Log.w(TAG, "YIN error (ignored): ${e.message}")
|
|
205
|
+
return true
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
override fun processingFinished() {
|
|
209
|
+
yinPitchProcessor.processingFinished()
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
mpmDispatcher = AudioDispatcherFactory.fromDefaultMicrophone(sampleRate, mpmBufferSize, mpmOverlap)
|
|
214
|
+
mpmDispatcher?.addAudioProcessor(safeYinProcessor)
|
|
215
|
+
|
|
216
|
+
logStatus("debug", "YIN dispatcher prepared (mid/high notes ${mpmMinHz}-${mpmMaxHz}Hz)")
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private fun emitPitch(pitchInHz: Float, decibel: Float) {
|
|
220
|
+
if (pitchInHz <= 0 || pitchInHz.isNaN()) return
|
|
221
|
+
|
|
78
222
|
val midiNote = 12 * log2(pitchInHz / 440f) + 69
|
|
79
223
|
val roundedMidiNote = round(midiNote).toInt()
|
|
80
|
-
|
|
81
|
-
// Calculate note index (0-11) and octave
|
|
82
224
|
val noteIndex = ((roundedMidiNote % 12) + 12) % 12
|
|
83
225
|
val octave = (roundedMidiNote / 12) - 1
|
|
84
|
-
|
|
85
|
-
// Calculate offset from perfect pitch (in cents, then convert to percentage)
|
|
86
|
-
// 100 cents = 1 semitone, so we express as percentage of a semitone
|
|
87
226
|
val centsOff = (midiNote - roundedMidiNote) * 100
|
|
88
|
-
val offsetPercentage = centsOff // Already in a reasonable range (-50 to +50)
|
|
89
227
|
|
|
90
|
-
|
|
228
|
+
// Pre-filter: JS discards offset > 25% anyway, so skip detections > 40%
|
|
229
|
+
// to reduce noise in the JS majority voting window
|
|
230
|
+
if (kotlin.math.abs(centsOff) > 40f) return
|
|
91
231
|
|
|
92
|
-
|
|
93
|
-
note =
|
|
232
|
+
onPitchDetected(PitchData(
|
|
233
|
+
note = notes[noteIndex],
|
|
94
234
|
octave = octave,
|
|
95
235
|
frequency = pitchInHz,
|
|
96
236
|
amplitude = decibel,
|
|
97
|
-
offset =
|
|
98
|
-
)
|
|
99
|
-
|
|
100
|
-
onPitchDetected(pitchData)
|
|
237
|
+
offset = centsOff
|
|
238
|
+
))
|
|
101
239
|
}
|
|
102
240
|
|
|
103
241
|
fun setOnPitchDetectedListener(listener: (PitchData) -> Unit) {
|
|
@@ -122,53 +260,63 @@ class PitchAnalyzer {
|
|
|
122
260
|
this.levelThreshold = threshold
|
|
123
261
|
}
|
|
124
262
|
|
|
263
|
+
fun setSampleRate(rate: Int) {
|
|
264
|
+
this.sampleRate = rate
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
fun getSampleRate(): Int = this.sampleRate
|
|
268
|
+
|
|
125
269
|
fun setBufferSize(size: Int) {
|
|
126
270
|
this.bufferSize = size
|
|
127
271
|
}
|
|
128
272
|
|
|
129
|
-
fun getBufferSize(): Int
|
|
130
|
-
return this.bufferSize
|
|
131
|
-
}
|
|
273
|
+
fun getBufferSize(): Int = this.bufferSize
|
|
132
274
|
|
|
133
275
|
fun setAlgorithm(name: String) {
|
|
134
276
|
this.algorithmName = name.lowercase()
|
|
135
|
-
this.algorithm = when (this.algorithmName) {
|
|
136
|
-
"yin" -> PitchEstimationAlgorithm.YIN
|
|
137
|
-
"fft_yin" -> PitchEstimationAlgorithm.FFT_YIN
|
|
138
|
-
"mpm" -> PitchEstimationAlgorithm.MPM
|
|
139
|
-
"fft_pitch" -> PitchEstimationAlgorithm.FFT_PITCH
|
|
140
|
-
"dynamic_wavelet" -> PitchEstimationAlgorithm.DYNAMIC_WAVELET
|
|
141
|
-
"amdf" -> PitchEstimationAlgorithm.AMDF
|
|
142
|
-
else -> PitchEstimationAlgorithm.FFT_YIN // Default
|
|
143
|
-
}
|
|
144
277
|
}
|
|
145
278
|
|
|
146
|
-
fun getAlgorithm(): String
|
|
147
|
-
return this.algorithmName
|
|
148
|
-
}
|
|
279
|
+
fun getAlgorithm(): String = this.algorithmName
|
|
149
280
|
|
|
150
281
|
fun start() {
|
|
151
282
|
try {
|
|
152
|
-
logStatus("debug", "start() called")
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
283
|
+
logStatus("debug", "start() called — dual dispatcher mode (HPS + MPM)")
|
|
284
|
+
|
|
285
|
+
// Start HPS dispatcher first
|
|
286
|
+
prepareHPS()
|
|
287
|
+
hpsThread = Thread(hpsDispatcher)
|
|
288
|
+
hpsThread?.name = "HPS-Thread"
|
|
289
|
+
hpsThread?.start()
|
|
290
|
+
logStatus("debug", "HPS thread started")
|
|
291
|
+
|
|
292
|
+
// Start MPM dispatcher second
|
|
293
|
+
prepareMPM()
|
|
294
|
+
mpmThread = Thread(mpmDispatcher)
|
|
295
|
+
mpmThread?.name = "MPM-Thread"
|
|
296
|
+
mpmThread?.start()
|
|
297
|
+
logStatus("debug", "MPM thread started")
|
|
298
|
+
|
|
157
299
|
isRecording = true
|
|
158
|
-
logStatus("debug", "
|
|
300
|
+
logStatus("debug", "Dual dispatcher recording started successfully")
|
|
159
301
|
} catch (e: Exception) {
|
|
160
302
|
logStatus("error", "Error in start(): ${e.message}")
|
|
303
|
+
// Clean up whatever was started
|
|
304
|
+
stop()
|
|
161
305
|
isRecording = false
|
|
162
306
|
}
|
|
163
307
|
}
|
|
164
308
|
|
|
165
309
|
fun stop() {
|
|
166
|
-
|
|
167
|
-
|
|
310
|
+
hpsDispatcher?.stop()
|
|
311
|
+
mpmDispatcher?.stop()
|
|
312
|
+
hpsThread?.interrupt()
|
|
313
|
+
mpmThread?.interrupt()
|
|
314
|
+
hpsDispatcher = null
|
|
315
|
+
mpmDispatcher = null
|
|
316
|
+
hpsThread = null
|
|
317
|
+
mpmThread = null
|
|
168
318
|
isRecording = false
|
|
169
319
|
}
|
|
170
320
|
|
|
171
|
-
fun isRecording(): Boolean
|
|
172
|
-
|
|
173
|
-
}
|
|
174
|
-
}
|
|
321
|
+
fun isRecording(): Boolean = isRecording
|
|
322
|
+
}
|
|
@@ -58,6 +58,14 @@ class ReactNativeSimpleNotePitchDetectorModule : Module() {
|
|
|
58
58
|
pitchAnalyzer.setLevelThreshold(threshold.toFloat())
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
Function("setSampleRate") { rate: Int ->
|
|
62
|
+
pitchAnalyzer.setSampleRate(rate)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
Function("getSampleRate") {
|
|
66
|
+
pitchAnalyzer.getSampleRate()
|
|
67
|
+
}
|
|
68
|
+
|
|
61
69
|
// Allow JS to configure the buffer size
|
|
62
70
|
// Must be called before start() to take effect
|
|
63
71
|
// Common values: 1024 (better for high frequencies), 2048 (balanced), 4096 (better for low frequencies)
|
|
@@ -8,7 +8,4 @@ export type ChangeEventPayload = {
|
|
|
8
8
|
/** Offset from perfect pitch as percentage (-50 to +50, negative = flat, positive = sharp) */
|
|
9
9
|
offset: number;
|
|
10
10
|
};
|
|
11
|
-
export type ReactNativeSimpleNotePitchDetectorViewProps = {
|
|
12
|
-
name: string;
|
|
13
|
-
};
|
|
14
11
|
//# sourceMappingURL=ReactNativeSimpleNotePitchDetector.types.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ReactNativeSimpleNotePitchDetector.types.d.ts","sourceRoot":"","sources":["../src/ReactNativeSimpleNotePitchDetector.types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,kBAAkB,GAAG;IAC/B,uDAAuD;IACvD,IAAI,EAAE,MAAM,CAAC;IACb,2CAA2C;IAC3C,MAAM,EAAE,MAAM,CAAC;IACf,0BAA0B;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,8FAA8F;IAC9F,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC
|
|
1
|
+
{"version":3,"file":"ReactNativeSimpleNotePitchDetector.types.d.ts","sourceRoot":"","sources":["../src/ReactNativeSimpleNotePitchDetector.types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,kBAAkB,GAAG;IAC/B,uDAAuD;IACvD,IAAI,EAAE,MAAM,CAAC;IACb,2CAA2C;IAC3C,MAAM,EAAE,MAAM,CAAC;IACf,0BAA0B;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,8FAA8F;IAC9F,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ReactNativeSimpleNotePitchDetector.types.js","sourceRoot":"","sources":["../src/ReactNativeSimpleNotePitchDetector.types.ts"],"names":[],"mappings":"","sourcesContent":["export type ChangeEventPayload = {\n /** Note name without octave (e.g., \"C#\", \"D\", \"Eb\") */\n note: string;\n /** Octave number (e.g., 4 for middle C) */\n octave: number;\n /** Raw frequency in Hz */\n frequency: number;\n /** Offset from perfect pitch as percentage (-50 to +50, negative = flat, positive = sharp) */\n offset: number;\n};\n
|
|
1
|
+
{"version":3,"file":"ReactNativeSimpleNotePitchDetector.types.js","sourceRoot":"","sources":["../src/ReactNativeSimpleNotePitchDetector.types.ts"],"names":[],"mappings":"","sourcesContent":["export type ChangeEventPayload = {\n /** Note name without octave (e.g., \"C#\", \"D\", \"Eb\") */\n note: string;\n /** Octave number (e.g., 4 for middle C) */\n octave: number;\n /** Raw frequency in Hz */\n frequency: number;\n /** Offset from perfect pitch as percentage (-50 to +50, negative = flat, positive = sharp) */\n offset: number;\n};\n"]}
|
package/build/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { ChangeEventPayload
|
|
1
|
+
import { EventSubscription } from "expo-modules-core";
|
|
2
|
+
import { ChangeEventPayload } from "./ReactNativeSimpleNotePitchDetector.types";
|
|
3
3
|
export declare function start(): any;
|
|
4
4
|
export declare function stop(): any;
|
|
5
5
|
export declare function isRecording(): any;
|
|
@@ -66,7 +66,7 @@ export declare function setAlgorithm(algorithm: string): any;
|
|
|
66
66
|
* @returns Current algorithm name
|
|
67
67
|
*/
|
|
68
68
|
export declare function getAlgorithm(): string;
|
|
69
|
-
export declare function onChangeNote(listener: (event: ChangeEventPayload) => void):
|
|
69
|
+
export declare function onChangeNote(listener: (event: ChangeEventPayload) => void): EventSubscription;
|
|
70
70
|
export interface StatusEventPayload {
|
|
71
71
|
level: "debug" | "error" | "verbose";
|
|
72
72
|
message: string;
|
|
@@ -76,6 +76,6 @@ export interface StatusEventPayload {
|
|
|
76
76
|
* Useful for debugging audio initialization issues.
|
|
77
77
|
* @param listener - Callback that receives status events with level and message
|
|
78
78
|
*/
|
|
79
|
-
export declare function onStatus(listener: (event: StatusEventPayload) => void):
|
|
80
|
-
export {
|
|
79
|
+
export declare function onStatus(listener: (event: StatusEventPayload) => void): EventSubscription;
|
|
80
|
+
export { ChangeEventPayload };
|
|
81
81
|
//# sourceMappingURL=index.d.ts.map
|
package/build/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,iBAAiB,EAClB,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EAAE,kBAAkB,EAAE,MAAM,4CAA4C,CAAC;AAEhF,wBAAgB,KAAK,QAEpB;AAED,wBAAgB,IAAI,QAEnB;AAED,wBAAgB,WAAW,QAE1B;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,OAElD;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,OAEzC;AAED;;;GAGG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,OAE7C;AAED;;;GAGG;AACH,wBAAgB,YAAY,IAAI,MAAM,CAErC;AAED,wBAAgB,YAAY,CAC1B,QAAQ,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,IAAI,GAC5C,iBAAiB,CAEnB;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,OAAO,GAAG,OAAO,GAAG,SAAS,CAAC;IACrC,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,CACtB,QAAQ,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,IAAI,GAC5C,iBAAiB,CAEnB;AAED,OAAO,EAAE,kBAAkB,EAAE,CAAC"}
|
package/build/index.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { NativeModulesProxy, EventEmitter, } from "expo-modules-core";
|
|
2
1
|
import ReactNativeSimpleNotePitchDetectorModule from "./ReactNativeSimpleNotePitchDetectorModule";
|
|
3
2
|
export function start() {
|
|
4
3
|
return ReactNativeSimpleNotePitchDetectorModule.start();
|
|
@@ -82,10 +81,8 @@ export function setAlgorithm(algorithm) {
|
|
|
82
81
|
export function getAlgorithm() {
|
|
83
82
|
return ReactNativeSimpleNotePitchDetectorModule.getAlgorithm();
|
|
84
83
|
}
|
|
85
|
-
const emitter = new EventEmitter(ReactNativeSimpleNotePitchDetectorModule ??
|
|
86
|
-
NativeModulesProxy.ReactNativeSimpleNotePitchDetector);
|
|
87
84
|
export function onChangeNote(listener) {
|
|
88
|
-
return
|
|
85
|
+
return ReactNativeSimpleNotePitchDetectorModule.addListener("onChangeNote", listener);
|
|
89
86
|
}
|
|
90
87
|
/**
|
|
91
88
|
* Listen for status/debug messages from the native module.
|
|
@@ -93,6 +90,6 @@ export function onChangeNote(listener) {
|
|
|
93
90
|
* @param listener - Callback that receives status events with level and message
|
|
94
91
|
*/
|
|
95
92
|
export function onStatus(listener) {
|
|
96
|
-
return
|
|
93
|
+
return ReactNativeSimpleNotePitchDetectorModule.addListener("onStatus", listener);
|
|
97
94
|
}
|
|
98
95
|
//# sourceMappingURL=index.js.map
|
package/build/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,wCAAwC,MAAM,4CAA4C,CAAC;AAGlG,MAAM,UAAU,KAAK;IACnB,OAAO,wCAAwC,CAAC,KAAK,EAAE,CAAC;AAC1D,CAAC;AAED,MAAM,UAAU,IAAI;IAClB,OAAO,wCAAwC,CAAC,IAAI,EAAE,CAAC;AACzD,CAAC;AAED,MAAM,UAAU,WAAW;IACzB,OAAO,wCAAwC,CAAC,WAAW,EAAE,CAAC;AAChE,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAAC,SAAiB;IACjD,OAAO,wCAAwC,CAAC,iBAAiB,CAAC,SAAS,CAAC,CAAC;AAC/E,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,OAAO,wCAAwC,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC;AACtE,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa;IAC3B,OAAO,wCAAwC,CAAC,aAAa,EAAE,CAAC;AAClE,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,UAAU,YAAY,CAAC,SAAiB;IAC5C,OAAO,wCAAwC,CAAC,YAAY,CAAC,SAAS,CAAC,CAAC;AAC1E,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,YAAY;IAC1B,OAAO,wCAAwC,CAAC,YAAY,EAAE,CAAC;AACjE,CAAC;AAED,MAAM,UAAU,YAAY,CAC1B,QAA6C;IAE7C,OAAO,wCAAwC,CAAC,WAAW,CAAC,cAAc,EAAE,QAAQ,CAAC,CAAC;AACxF,CAAC;AAOD;;;;GAIG;AACH,MAAM,UAAU,QAAQ,CACtB,QAA6C;IAE7C,OAAO,wCAAwC,CAAC,WAAW,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;AACpF,CAAC","sourcesContent":["import {\n EventSubscription,\n} from \"expo-modules-core\";\n\nimport ReactNativeSimpleNotePitchDetectorModule from \"./ReactNativeSimpleNotePitchDetectorModule\";\nimport { ChangeEventPayload } from \"./ReactNativeSimpleNotePitchDetector.types\";\n\nexport function start() {\n return ReactNativeSimpleNotePitchDetectorModule.start();\n}\n\nexport function stop() {\n return ReactNativeSimpleNotePitchDetectorModule.stop();\n}\n\nexport function isRecording() {\n return ReactNativeSimpleNotePitchDetectorModule.isRecording();\n}\n\n/**\n * Set the minimum audio level threshold for pitch detection.\n * Values are in dB (e.g., -30 means sounds quieter than -30dB are ignored).\n * Lower values = more sensitive (picks up quieter sounds).\n * Default is -30.\n * @param threshold - Level threshold in dB (e.g., -30, -35, -40)\n */\nexport function setLevelThreshold(threshold: number) {\n return ReactNativeSimpleNotePitchDetectorModule.setLevelThreshold(threshold);\n}\n\n/**\n * Set the buffer size for pitch detection.\n * Must be called before start() or will restart the engine if already running.\n *\n * Buffer size affects the trade-off between frequency range and responsiveness:\n * - Smaller buffer (2048-4096): Better for high frequencies, faster response, but less accurate for low notes\n * - Larger buffer (8192-16384): Better for low frequencies, more accurate, but slightly more latency\n *\n * iOS defaults to 8192, Android defaults to 2048.\n *\n * Recommended values:\n * - 4096: Good balance for most use cases\n * - 8192: Better for bass instruments or full piano range (iOS default)\n * - 2048: Better for high-pitched instruments, faster response (Android default)\n *\n * @param size - Buffer size (must be power of 2: 1024, 2048, 4096, 8192, 16384)\n */\nexport function setBufferSize(size: number) {\n return ReactNativeSimpleNotePitchDetectorModule.setBufferSize(size);\n}\n\n/**\n * Get the current buffer size.\n * @returns Current buffer size\n */\nexport function getBufferSize(): number {\n return ReactNativeSimpleNotePitchDetectorModule.getBufferSize();\n}\n\n/**\n * Set the pitch estimation algorithm.\n * Must be called before start() or will restart the engine if already running.\n *\n * Available algorithms differ by platform:\n *\n * **iOS (Beethoven library):**\n * - \"yin\" (default) - YIN algorithm, good for monophonic instruments\n * - \"hps\" - Harmonic Product Spectrum\n * - \"barycentric\" - Barycentric interpolation\n * - \"quadradic\" - Quadratic interpolation\n * - \"jains\" - Jain's method\n * - \"quinnsFirst\" - Quinn's first estimator\n * - \"quinnsSecond\" - Quinn's second estimator\n * - \"maxValue\" - Maximum value method\n *\n * **Android (TarsosDSP library):**\n * - \"fft_yin\" (default) - FFT-based YIN, faster than pure YIN\n * - \"yin\" - Pure YIN algorithm\n * - \"mpm\" - McLeod Pitch Method, good for speech and music\n * - \"fft_pitch\" - FFT bin with most energy\n * - \"dynamic_wavelet\" - Dynamic wavelet algorithm\n * - \"amdf\" - Average Magnitude Difference Function\n *\n * @param algorithm - Algorithm name (case-insensitive)\n */\nexport function setAlgorithm(algorithm: string) {\n return ReactNativeSimpleNotePitchDetectorModule.setAlgorithm(algorithm);\n}\n\n/**\n * Get the current algorithm name.\n * @returns Current algorithm name\n */\nexport function getAlgorithm(): string {\n return ReactNativeSimpleNotePitchDetectorModule.getAlgorithm();\n}\n\nexport function onChangeNote(\n listener: (event: ChangeEventPayload) => void\n): EventSubscription {\n return ReactNativeSimpleNotePitchDetectorModule.addListener(\"onChangeNote\", listener);\n}\n\nexport interface StatusEventPayload {\n level: \"debug\" | \"error\" | \"verbose\";\n message: string;\n}\n\n/**\n * Listen for status/debug messages from the native module.\n * Useful for debugging audio initialization issues.\n * @param listener - Callback that receives status events with level and message\n */\nexport function onStatus(\n listener: (event: StatusEventPayload) => void\n): EventSubscription {\n return ReactNativeSimpleNotePitchDetectorModule.addListener(\"onStatus\", listener);\n}\n\nexport { ChangeEventPayload };\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-simple-note-pitch-detector",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "a simple react native library to detect the pitch of the input recording",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
"prepublishOnly": "expo-module prepublishOnly",
|
|
14
14
|
"expo-module": "expo-module",
|
|
15
15
|
"open:ios": "open -a \"Xcode\" example/ios",
|
|
16
|
-
"open:android": "open -a \"Android Studio\" example/android"
|
|
16
|
+
"open:android": "open -a \"Android Studio\" example/android",
|
|
17
|
+
"android": "expo run:android",
|
|
18
|
+
"ios": "expo run:ios"
|
|
17
19
|
},
|
|
18
20
|
"keywords": [
|
|
19
21
|
"react-native",
|
|
@@ -30,9 +32,9 @@
|
|
|
30
32
|
"homepage": "https://github.com/derekhdawson/react-native-simple-note-pitch-detector#readme",
|
|
31
33
|
"dependencies": {},
|
|
32
34
|
"devDependencies": {
|
|
33
|
-
"@types/react": "
|
|
35
|
+
"@types/react": "~19.1.10",
|
|
34
36
|
"expo-module-scripts": "^3.0.11",
|
|
35
|
-
"expo-modules-core": "
|
|
37
|
+
"expo-modules-core": "~3.0.29"
|
|
36
38
|
},
|
|
37
39
|
"peerDependencies": {
|
|
38
40
|
"expo": "*",
|
package/src/index.ts
CHANGED
|
@@ -1,14 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
EventEmitter,
|
|
4
|
-
Subscription,
|
|
2
|
+
EventSubscription,
|
|
5
3
|
} from "expo-modules-core";
|
|
6
4
|
|
|
7
5
|
import ReactNativeSimpleNotePitchDetectorModule from "./ReactNativeSimpleNotePitchDetectorModule";
|
|
8
|
-
import {
|
|
9
|
-
ChangeEventPayload,
|
|
10
|
-
ReactNativeSimpleNotePitchDetectorViewProps,
|
|
11
|
-
} from "./ReactNativeSimpleNotePitchDetector.types";
|
|
6
|
+
import { ChangeEventPayload } from "./ReactNativeSimpleNotePitchDetector.types";
|
|
12
7
|
|
|
13
8
|
export function start() {
|
|
14
9
|
return ReactNativeSimpleNotePitchDetectorModule.start();
|
|
@@ -100,15 +95,10 @@ export function getAlgorithm(): string {
|
|
|
100
95
|
return ReactNativeSimpleNotePitchDetectorModule.getAlgorithm();
|
|
101
96
|
}
|
|
102
97
|
|
|
103
|
-
const emitter = new EventEmitter(
|
|
104
|
-
ReactNativeSimpleNotePitchDetectorModule ??
|
|
105
|
-
NativeModulesProxy.ReactNativeSimpleNotePitchDetector
|
|
106
|
-
);
|
|
107
|
-
|
|
108
98
|
export function onChangeNote(
|
|
109
99
|
listener: (event: ChangeEventPayload) => void
|
|
110
|
-
):
|
|
111
|
-
return
|
|
100
|
+
): EventSubscription {
|
|
101
|
+
return ReactNativeSimpleNotePitchDetectorModule.addListener("onChangeNote", listener);
|
|
112
102
|
}
|
|
113
103
|
|
|
114
104
|
export interface StatusEventPayload {
|
|
@@ -123,8 +113,8 @@ export interface StatusEventPayload {
|
|
|
123
113
|
*/
|
|
124
114
|
export function onStatus(
|
|
125
115
|
listener: (event: StatusEventPayload) => void
|
|
126
|
-
):
|
|
127
|
-
return
|
|
116
|
+
): EventSubscription {
|
|
117
|
+
return ReactNativeSimpleNotePitchDetectorModule.addListener("onStatus", listener);
|
|
128
118
|
}
|
|
129
119
|
|
|
130
|
-
export {
|
|
120
|
+
export { ChangeEventPayload };
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
File without changes
|
|
Binary file
|
|
File without changes
|