react-native-simple-note-pitch-detector 0.4.4 → 0.5.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 +43 -51
- package/android/src/main/java/expo/modules/simplenotepitchdetector/ReactNativeSimpleNotePitchDetectorModule.kt +13 -3
- package/build/ReactNativeSimpleNotePitchDetector.types.d.ts +7 -0
- package/build/ReactNativeSimpleNotePitchDetector.types.d.ts.map +1 -1
- package/build/ReactNativeSimpleNotePitchDetector.types.js.map +1 -1
- package/build/index.d.ts +8 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +10 -0
- package/build/index.js.map +1 -1
- package/ios/ReactNativeSimpleNotePitchDetectorModule.swift +33 -45
- package/package.json +2 -2
- package/src/ReactNativeSimpleNotePitchDetector.types.ts +7 -0
- package/src/index.ts +11 -0
- package/android/.gradle/8.10/checksums/checksums.lock +0 -0
- package/android/.gradle/8.10/dependencies-accessors/gc.properties +0 -0
- package/android/.gradle/8.10/fileChanges/last-build.bin +0 -0
- package/android/.gradle/8.10/fileHashes/fileHashes.bin +0 -0
- package/android/.gradle/8.10/fileHashes/fileHashes.lock +0 -0
- package/android/.gradle/8.10/gc.properties +0 -0
- package/android/.gradle/8.9/checksums/checksums.lock +0 -0
- package/android/.gradle/8.9/dependencies-accessors/gc.properties +0 -0
- package/android/.gradle/8.9/fileChanges/last-build.bin +0 -0
- package/android/.gradle/8.9/fileHashes/fileHashes.lock +0 -0
- package/android/.gradle/8.9/gc.properties +0 -0
- package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
- package/android/.gradle/buildOutputCleanup/cache.properties +0 -2
- package/android/.gradle/nb-cache/trust/CAEFFA28704E1545FBB04BFE5CC58F4EF060708117C17DB37BCD57FB3462AE23 +0 -1
- package/android/.gradle/vcs-1/gc.properties +0 -0
|
@@ -8,20 +8,24 @@ import be.tarsos.dsp.pitch.PitchProcessor
|
|
|
8
8
|
import be.tarsos.dsp.pitch.PitchProcessor.PitchEstimationAlgorithm
|
|
9
9
|
import kotlin.math.log2
|
|
10
10
|
import kotlin.math.round
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
import kotlin.math.pow
|
|
12
|
+
import kotlin.math.abs
|
|
13
|
+
|
|
14
|
+
data class PitchData(
|
|
15
|
+
val note: String,
|
|
16
|
+
val octave: Int,
|
|
17
|
+
val frequency: Float,
|
|
18
|
+
val amplitude: Float,
|
|
19
|
+
val offset: Float
|
|
15
20
|
)
|
|
16
21
|
|
|
17
22
|
class PitchAnalyzer {
|
|
18
23
|
|
|
19
24
|
private val notes = arrayOf("C","C#","D","D#","E","F","F#","G","G#","A","A#","B")
|
|
20
|
-
private val notesBuffer = Array(6) { NoteAndDecibel() }
|
|
21
|
-
private var counter = 0
|
|
22
25
|
|
|
23
|
-
private lateinit var
|
|
26
|
+
private lateinit var onPitchDetected: (PitchData) -> Unit
|
|
24
27
|
private var isRecording = false
|
|
28
|
+
private var levelThreshold = -30f
|
|
25
29
|
|
|
26
30
|
private var dispatcher: AudioDispatcher? = null
|
|
27
31
|
private var processor: AudioProcessor? = null
|
|
@@ -30,7 +34,8 @@ class PitchAnalyzer {
|
|
|
30
34
|
private val handler = PitchDetectionHandler { res, e ->
|
|
31
35
|
val pitchInHz = res.pitch
|
|
32
36
|
val decibel = e.getdBSPL().toFloat()
|
|
33
|
-
if
|
|
37
|
+
// Only process if above the level threshold
|
|
38
|
+
if (decibel > levelThreshold) {
|
|
34
39
|
process(pitchInHz, decibel)
|
|
35
40
|
}
|
|
36
41
|
}
|
|
@@ -43,29 +48,42 @@ class PitchAnalyzer {
|
|
|
43
48
|
}
|
|
44
49
|
|
|
45
50
|
private fun process(pitchInHz: Float, decibel: Float) {
|
|
46
|
-
|
|
51
|
+
if (pitchInHz <= 0 || pitchInHz.isNaN()) {
|
|
52
|
+
return
|
|
53
|
+
}
|
|
47
54
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
55
|
+
// Calculate MIDI note number (A4 = 440Hz = MIDI 69)
|
|
56
|
+
val midiNote = 12 * log2(pitchInHz / 440f) + 69
|
|
57
|
+
val roundedMidiNote = round(midiNote).toInt()
|
|
51
58
|
|
|
52
|
-
|
|
53
|
-
|
|
59
|
+
// Calculate note index (0-11) and octave
|
|
60
|
+
val noteIndex = ((roundedMidiNote % 12) + 12) % 12
|
|
61
|
+
val octave = (roundedMidiNote / 12) - 1
|
|
54
62
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
63
|
+
// Calculate offset from perfect pitch (in cents, then convert to percentage)
|
|
64
|
+
// 100 cents = 1 semitone, so we express as percentage of a semitone
|
|
65
|
+
val centsOff = (midiNote - roundedMidiNote) * 100
|
|
66
|
+
val offsetPercentage = centsOff // Already in a reasonable range (-50 to +50)
|
|
58
67
|
|
|
59
|
-
|
|
60
|
-
}
|
|
68
|
+
val note = notes[noteIndex]
|
|
61
69
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
70
|
+
val pitchData = PitchData(
|
|
71
|
+
note = note,
|
|
72
|
+
octave = octave,
|
|
73
|
+
frequency = pitchInHz,
|
|
74
|
+
amplitude = decibel,
|
|
75
|
+
offset = offsetPercentage
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
onPitchDetected(pitchData)
|
|
65
79
|
}
|
|
66
80
|
|
|
67
|
-
fun
|
|
68
|
-
this.
|
|
81
|
+
fun setOnPitchDetectedListener(listener: (PitchData) -> Unit) {
|
|
82
|
+
this.onPitchDetected = listener
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
fun setLevelThreshold(threshold: Float) {
|
|
86
|
+
this.levelThreshold = threshold
|
|
69
87
|
}
|
|
70
88
|
|
|
71
89
|
fun start() {
|
|
@@ -74,6 +92,7 @@ class PitchAnalyzer {
|
|
|
74
92
|
runner?.start()
|
|
75
93
|
isRecording = true
|
|
76
94
|
}
|
|
95
|
+
|
|
77
96
|
fun stop() {
|
|
78
97
|
dispatcher?.stop()
|
|
79
98
|
runner?.interrupt()
|
|
@@ -83,31 +102,4 @@ class PitchAnalyzer {
|
|
|
83
102
|
fun isRecording(): Boolean {
|
|
84
103
|
return isRecording
|
|
85
104
|
}
|
|
86
|
-
|
|
87
|
-
private fun getMostFrequentNote(notes: Array<NoteAndDecibel>) : String? {
|
|
88
|
-
val noteToDecibel = mutableMapOf<String, Float>();
|
|
89
|
-
notes.forEach {
|
|
90
|
-
if (!noteToDecibel.containsKey(it.note)) {
|
|
91
|
-
noteToDecibel[it.note] = 0f
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
noteToDecibel[it.note] = noteToDecibel[it.note]!! + it.decibel
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
var mostFrequentNote = ""
|
|
98
|
-
var maxDecibel = 0f
|
|
99
|
-
|
|
100
|
-
noteToDecibel.forEach {
|
|
101
|
-
if (mostFrequentNote == "" || it.value < maxDecibel) {
|
|
102
|
-
mostFrequentNote = it.key
|
|
103
|
-
maxDecibel = it.value
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
if (mostFrequentNote == "") {
|
|
108
|
-
return null
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
return mostFrequentNote
|
|
112
|
-
}
|
|
113
105
|
}
|
|
@@ -3,6 +3,7 @@ package expo.modules.simplenotepitchdetector
|
|
|
3
3
|
import expo.modules.kotlin.modules.Module
|
|
4
4
|
import expo.modules.kotlin.modules.ModuleDefinition
|
|
5
5
|
import expo.modules.simplepitchdetector.PitchAnalyzer
|
|
6
|
+
import expo.modules.simplepitchdetector.PitchData
|
|
6
7
|
import androidx.core.os.bundleOf
|
|
7
8
|
|
|
8
9
|
class ReactNativeSimpleNotePitchDetectorModule : Module() {
|
|
@@ -20,10 +21,15 @@ class ReactNativeSimpleNotePitchDetectorModule : Module() {
|
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
Function("start") {
|
|
23
|
-
pitchAnalyzer.
|
|
24
|
+
pitchAnalyzer.setOnPitchDetectedListener { pitchData: PitchData ->
|
|
24
25
|
this@ReactNativeSimpleNotePitchDetectorModule.sendEvent(
|
|
25
26
|
"onChangeNote",
|
|
26
|
-
bundleOf(
|
|
27
|
+
bundleOf(
|
|
28
|
+
"note" to pitchData.note,
|
|
29
|
+
"octave" to pitchData.octave,
|
|
30
|
+
"frequency" to pitchData.frequency.toDouble(),
|
|
31
|
+
"offset" to pitchData.offset.toDouble()
|
|
32
|
+
)
|
|
27
33
|
)
|
|
28
34
|
}
|
|
29
35
|
|
|
@@ -31,7 +37,11 @@ class ReactNativeSimpleNotePitchDetectorModule : Module() {
|
|
|
31
37
|
}
|
|
32
38
|
|
|
33
39
|
Function("stop") {
|
|
34
|
-
|
|
40
|
+
pitchAnalyzer.stop()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
Function("setLevelThreshold") { threshold: Double ->
|
|
44
|
+
pitchAnalyzer.setLevelThreshold(threshold.toFloat())
|
|
35
45
|
}
|
|
36
46
|
}
|
|
37
47
|
}
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
export type ChangeEventPayload = {
|
|
2
|
+
/** Note name without octave (e.g., "C#", "D", "Eb") */
|
|
2
3
|
note: string;
|
|
4
|
+
/** Octave number (e.g., 4 for middle C) */
|
|
5
|
+
octave: number;
|
|
6
|
+
/** Raw frequency in Hz */
|
|
7
|
+
frequency: number;
|
|
8
|
+
/** Offset from perfect pitch as percentage (-50 to +50, negative = flat, positive = sharp) */
|
|
9
|
+
offset: number;
|
|
3
10
|
};
|
|
4
11
|
export type ReactNativeSimpleNotePitchDetectorViewProps = {
|
|
5
12
|
name: string;
|
|
@@ -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,IAAI,EAAE,MAAM,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;AAEF,MAAM,MAAM,2CAA2C,GAAG;IACxD,IAAI,EAAE,MAAM,CAAC;CACd,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ReactNativeSimpleNotePitchDetector.types.js","sourceRoot":"","sources":["../src/ReactNativeSimpleNotePitchDetector.types.ts"],"names":[],"mappings":"","sourcesContent":["export type ChangeEventPayload = {\n note: string;\n};\n\nexport type ReactNativeSimpleNotePitchDetectorViewProps = {\n name: string;\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\nexport type ReactNativeSimpleNotePitchDetectorViewProps = {\n name: string;\n};\n"]}
|
package/build/index.d.ts
CHANGED
|
@@ -3,6 +3,14 @@ import { ChangeEventPayload, ReactNativeSimpleNotePitchDetectorViewProps } from
|
|
|
3
3
|
export declare function start(): any;
|
|
4
4
|
export declare function stop(): any;
|
|
5
5
|
export declare function isRecording(): any;
|
|
6
|
+
/**
|
|
7
|
+
* Set the minimum audio level threshold for pitch detection.
|
|
8
|
+
* Values are in dB (e.g., -30 means sounds quieter than -30dB are ignored).
|
|
9
|
+
* Lower values = more sensitive (picks up quieter sounds).
|
|
10
|
+
* Default is -30.
|
|
11
|
+
* @param threshold - Level threshold in dB (e.g., -30, -35, -40)
|
|
12
|
+
*/
|
|
13
|
+
export declare function setLevelThreshold(threshold: number): any;
|
|
6
14
|
export declare function onChangeNote(listener: (event: ChangeEventPayload) => void): Subscription;
|
|
7
15
|
export { ReactNativeSimpleNotePitchDetectorViewProps, ChangeEventPayload };
|
|
8
16
|
//# 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,EAGL,YAAY,EACb,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EACL,kBAAkB,EAClB,2CAA2C,EAC5C,MAAM,4CAA4C,CAAC;AAEpD,wBAAgB,KAAK,QAEpB;AAED,wBAAgB,IAAI,QAEnB;AAED,wBAAgB,WAAW,QAE1B;AAOD,wBAAgB,YAAY,CAC1B,QAAQ,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,IAAI,GAC5C,YAAY,CAEd;AAED,OAAO,EAAE,2CAA2C,EAAE,kBAAkB,EAAE,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,YAAY,EACb,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EACL,kBAAkB,EAClB,2CAA2C,EAC5C,MAAM,4CAA4C,CAAC;AAEpD,wBAAgB,KAAK,QAEpB;AAED,wBAAgB,IAAI,QAEnB;AAED,wBAAgB,WAAW,QAE1B;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,OAElD;AAOD,wBAAgB,YAAY,CAC1B,QAAQ,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,IAAI,GAC5C,YAAY,CAEd;AAED,OAAO,EAAE,2CAA2C,EAAE,kBAAkB,EAAE,CAAC"}
|
package/build/index.js
CHANGED
|
@@ -9,6 +9,16 @@ export function stop() {
|
|
|
9
9
|
export function isRecording() {
|
|
10
10
|
return ReactNativeSimpleNotePitchDetectorModule.isRecording();
|
|
11
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* Set the minimum audio level threshold for pitch detection.
|
|
14
|
+
* Values are in dB (e.g., -30 means sounds quieter than -30dB are ignored).
|
|
15
|
+
* Lower values = more sensitive (picks up quieter sounds).
|
|
16
|
+
* Default is -30.
|
|
17
|
+
* @param threshold - Level threshold in dB (e.g., -30, -35, -40)
|
|
18
|
+
*/
|
|
19
|
+
export function setLevelThreshold(threshold) {
|
|
20
|
+
return ReactNativeSimpleNotePitchDetectorModule.setLevelThreshold(threshold);
|
|
21
|
+
}
|
|
12
22
|
const emitter = new EventEmitter(ReactNativeSimpleNotePitchDetectorModule ??
|
|
13
23
|
NativeModulesProxy.ReactNativeSimpleNotePitchDetector);
|
|
14
24
|
export function onChangeNote(listener) {
|
package/build/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kBAAkB,EAClB,YAAY,GAEb,MAAM,mBAAmB,CAAC;AAE3B,OAAO,wCAAwC,MAAM,4CAA4C,CAAC;AAMlG,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,MAAM,OAAO,GAAG,IAAI,YAAY,CAC9B,wCAAwC;IACtC,kBAAkB,CAAC,kCAAkC,CACxD,CAAC;AAEF,MAAM,UAAU,YAAY,CAC1B,QAA6C;IAE7C,OAAO,OAAO,CAAC,WAAW,CAAqB,cAAc,EAAE,QAAQ,CAAC,CAAC;AAC3E,CAAC","sourcesContent":["import {\n NativeModulesProxy,\n EventEmitter,\n Subscription,\n} from \"expo-modules-core\";\n\nimport ReactNativeSimpleNotePitchDetectorModule from \"./ReactNativeSimpleNotePitchDetectorModule\";\nimport {\n ChangeEventPayload,\n ReactNativeSimpleNotePitchDetectorViewProps,\n} 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\nconst emitter = new EventEmitter(\n ReactNativeSimpleNotePitchDetectorModule ??\n NativeModulesProxy.ReactNativeSimpleNotePitchDetector\n);\n\nexport function onChangeNote(\n listener: (event: ChangeEventPayload) => void\n): Subscription {\n return emitter.addListener<ChangeEventPayload>(\"onChangeNote\", listener);\n}\n\nexport { ReactNativeSimpleNotePitchDetectorViewProps, ChangeEventPayload };\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kBAAkB,EAClB,YAAY,GAEb,MAAM,mBAAmB,CAAC;AAE3B,OAAO,wCAAwC,MAAM,4CAA4C,CAAC;AAMlG,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,MAAM,OAAO,GAAG,IAAI,YAAY,CAC9B,wCAAwC;IACtC,kBAAkB,CAAC,kCAAkC,CACxD,CAAC;AAEF,MAAM,UAAU,YAAY,CAC1B,QAA6C;IAE7C,OAAO,OAAO,CAAC,WAAW,CAAqB,cAAc,EAAE,QAAQ,CAAC,CAAC;AAC3E,CAAC","sourcesContent":["import {\n NativeModulesProxy,\n EventEmitter,\n Subscription,\n} from \"expo-modules-core\";\n\nimport ReactNativeSimpleNotePitchDetectorModule from \"./ReactNativeSimpleNotePitchDetectorModule\";\nimport {\n ChangeEventPayload,\n ReactNativeSimpleNotePitchDetectorViewProps,\n} 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\nconst emitter = new EventEmitter(\n ReactNativeSimpleNotePitchDetectorModule ??\n NativeModulesProxy.ReactNativeSimpleNotePitchDetector\n);\n\nexport function onChangeNote(\n listener: (event: ChangeEventPayload) => void\n): Subscription {\n return emitter.addListener<ChangeEventPayload>(\"onChangeNote\", listener);\n}\n\nexport { ReactNativeSimpleNotePitchDetectorViewProps, ChangeEventPayload };\n"]}
|
|
@@ -3,13 +3,8 @@ import Beethoven
|
|
|
3
3
|
import Pitchy
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
let NOTE_BUFFER_SIZE = 3
|
|
7
|
-
|
|
8
6
|
public class ReactNativeSimpleNotePitchDetectorModule: Module {
|
|
9
|
-
|
|
10
|
-
var notes = Array(repeating: "", count: NOTE_BUFFER_SIZE)
|
|
11
|
-
var counter = 0
|
|
12
|
-
|
|
7
|
+
|
|
13
8
|
public func definition() -> ModuleDefinition {
|
|
14
9
|
|
|
15
10
|
Name("ReactNativeSimpleNotePitchDetector")
|
|
@@ -23,69 +18,62 @@ public class ReactNativeSimpleNotePitchDetectorModule: Module {
|
|
|
23
18
|
Function("stop") {
|
|
24
19
|
pitchEngine.stop()
|
|
25
20
|
}
|
|
26
|
-
|
|
21
|
+
|
|
27
22
|
Function("isRecording") {
|
|
28
23
|
return pitchEngine.active
|
|
29
24
|
}
|
|
25
|
+
|
|
26
|
+
// Allow JS to configure the level threshold
|
|
27
|
+
Function("setLevelThreshold") { (threshold: Double) in
|
|
28
|
+
self.pitchEngine.levelThreshold = Float(threshold)
|
|
29
|
+
}
|
|
30
30
|
}
|
|
31
|
-
|
|
31
|
+
|
|
32
32
|
lazy var pitchEngine: PitchEngine = { [weak self] in
|
|
33
|
-
|
|
33
|
+
// Larger buffer (8192) = more accurate pitch detection, slightly more latency
|
|
34
|
+
// Using .yin which is generally best for monophonic instruments
|
|
35
|
+
let config = Config(bufferSize: 8192, estimationStrategy: .yin)
|
|
34
36
|
let pitchEngine = PitchEngine(config: config, delegate: self)
|
|
35
|
-
|
|
37
|
+
// Default threshold - can be adjusted from JS via setLevelThreshold()
|
|
38
|
+
pitchEngine.levelThreshold = -30
|
|
36
39
|
return pitchEngine
|
|
37
40
|
}()
|
|
38
|
-
|
|
39
|
-
func getMostFrequentNote(notes: [String]) -> String? {
|
|
40
|
-
var counts = [String: Int]()
|
|
41
|
-
|
|
42
|
-
notes.forEach { counts[$0] = (counts[$0] ?? 0) + 1 }
|
|
43
|
-
|
|
44
|
-
if let (value, count) = counts.max(by: {$0.1 < $1.1}) {
|
|
45
|
-
if (count <= 1) {
|
|
46
|
-
return nil
|
|
47
|
-
}
|
|
48
|
-
return value
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return nil
|
|
52
|
-
}
|
|
53
41
|
}
|
|
54
42
|
|
|
55
43
|
extension ReactNativeSimpleNotePitchDetectorModule: PitchEngineDelegate {
|
|
56
44
|
|
|
57
45
|
public func pitchEngine(_ pitchEngine: PitchEngine, didReceivePitch pitch: Pitch) {
|
|
58
|
-
|
|
46
|
+
|
|
47
|
+
let frequency = pitch.wave.frequency
|
|
48
|
+
|
|
49
|
+
// Offset from the nearest note (in percentage, can be negative or positive)
|
|
50
|
+
// Negative = flat, Positive = sharp
|
|
59
51
|
let offsetPercentage = pitch.closestOffset.percentage
|
|
60
|
-
let absOffsetPercentage = abs(offsetPercentage)
|
|
61
52
|
|
|
62
|
-
guard absOffsetPercentage > 1.0 else {
|
|
63
|
-
return
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (counter == notes.count) {
|
|
67
|
-
var note = getMostFrequentNote(notes: notes)
|
|
68
|
-
if (note != nil) {
|
|
69
|
-
self.sendEvent("onChangeNote", ["note" : note])
|
|
70
|
-
}
|
|
71
|
-
counter = 0
|
|
72
|
-
}
|
|
73
|
-
|
|
74
53
|
do {
|
|
75
|
-
let note = try Note(frequency:
|
|
54
|
+
let note = try Note(frequency: frequency)
|
|
76
55
|
let noteStr = note.string
|
|
77
|
-
|
|
78
|
-
|
|
56
|
+
// Remove the octave number (last character) to get just the note name
|
|
57
|
+
let noteName = String(noteStr.prefix(noteStr.count - 1))
|
|
58
|
+
let octave = note.octave
|
|
59
|
+
|
|
60
|
+
// Send all raw data to JS - let the app decide how to filter
|
|
61
|
+
self.sendEvent("onChangeNote", [
|
|
62
|
+
"note": noteName,
|
|
63
|
+
"octave": octave,
|
|
64
|
+
"frequency": frequency,
|
|
65
|
+
"offset": offsetPercentage
|
|
66
|
+
])
|
|
79
67
|
} catch {
|
|
80
|
-
|
|
68
|
+
// Could not determine note from frequency - skip
|
|
81
69
|
}
|
|
82
70
|
}
|
|
83
71
|
|
|
84
72
|
public func pitchEngine(_ pitchEngine: PitchEngine, didReceiveError error: Error) {
|
|
85
|
-
|
|
73
|
+
// Optionally send error events to JS
|
|
86
74
|
}
|
|
87
75
|
|
|
88
76
|
public func pitchEngineWentBelowLevelThreshold(_ pitchEngine: PitchEngine) {
|
|
89
|
-
|
|
77
|
+
// Audio dropped below threshold - could notify JS if needed
|
|
90
78
|
}
|
|
91
79
|
}
|
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.5.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",
|
|
@@ -39,4 +39,4 @@
|
|
|
39
39
|
"react": "*",
|
|
40
40
|
"react-native": "*"
|
|
41
41
|
}
|
|
42
|
-
}
|
|
42
|
+
}
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
export type ChangeEventPayload = {
|
|
2
|
+
/** Note name without octave (e.g., "C#", "D", "Eb") */
|
|
2
3
|
note: string;
|
|
4
|
+
/** Octave number (e.g., 4 for middle C) */
|
|
5
|
+
octave: number;
|
|
6
|
+
/** Raw frequency in Hz */
|
|
7
|
+
frequency: number;
|
|
8
|
+
/** Offset from perfect pitch as percentage (-50 to +50, negative = flat, positive = sharp) */
|
|
9
|
+
offset: number;
|
|
3
10
|
};
|
|
4
11
|
|
|
5
12
|
export type ReactNativeSimpleNotePitchDetectorViewProps = {
|
package/src/index.ts
CHANGED
|
@@ -22,6 +22,17 @@ export function isRecording() {
|
|
|
22
22
|
return ReactNativeSimpleNotePitchDetectorModule.isRecording();
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Set the minimum audio level threshold for pitch detection.
|
|
27
|
+
* Values are in dB (e.g., -30 means sounds quieter than -30dB are ignored).
|
|
28
|
+
* Lower values = more sensitive (picks up quieter sounds).
|
|
29
|
+
* Default is -30.
|
|
30
|
+
* @param threshold - Level threshold in dB (e.g., -30, -35, -40)
|
|
31
|
+
*/
|
|
32
|
+
export function setLevelThreshold(threshold: number) {
|
|
33
|
+
return ReactNativeSimpleNotePitchDetectorModule.setLevelThreshold(threshold);
|
|
34
|
+
}
|
|
35
|
+
|
|
25
36
|
const emitter = new EventEmitter(
|
|
26
37
|
ReactNativeSimpleNotePitchDetectorModule ??
|
|
27
38
|
NativeModulesProxy.ReactNativeSimpleNotePitchDetector
|
|
Binary file
|
|
File without changes
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
File without changes
|
|
Binary file
|
|
File without changes
|
|
Binary file
|
|
Binary file
|
|
File without changes
|
|
Binary file
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
7E27CB4C75232A88AD8EEDA14F3AA768C53AB046AD89E67E493D43A1C1B871E6
|
|
File without changes
|