@techoptio/react-native-live-pitch-detection 1.0.0
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/LICENSE +20 -0
- package/README.md +146 -0
- package/ReactNativeLivePitchDetection.podspec +21 -0
- package/android/build.gradle +95 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/java/io/techopt/lib/reactnativelivepitchdetection/ReactNativeLivePitchDetectionModule.kt +115 -0
- package/android/src/main/java/io/techopt/lib/reactnativelivepitchdetection/ReactNativeLivePitchDetectionPackage.kt +33 -0
- package/android/src/main/jni/CMakeLists.txt +15 -0
- package/android/src/main/jni/cpp-adapter.cpp +22 -0
- package/ios/ReactNativeLivePitchDetection.h +9 -0
- package/ios/ReactNativeLivePitchDetection.mm +127 -0
- package/lib/module/NativeReactNativeLivePitchDetection.js +5 -0
- package/lib/module/NativeReactNativeLivePitchDetection.js.map +1 -0
- package/lib/module/index.js +39 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/NativeReactNativeLivePitchDetection.d.ts +13 -0
- package/lib/typescript/src/NativeReactNativeLivePitchDetection.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +11 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +12 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/package.json +164 -0
- package/src/NativeReactNativeLivePitchDetection.ts +11 -0
- package/src/index.tsx +64 -0
- package/src/types.ts +13 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 TechOpt.io
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
in the Software without restriction, including without limitation the rights
|
|
7
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
furnished to do so, subject to the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# @techoptio/react-native-live-pitch-detection
|
|
2
|
+
|
|
3
|
+
Live pitch detection (frequency, note and octave) from device microphone for React Native.
|
|
4
|
+
|
|
5
|
+
This package was heavily inspired by, but not a direct fork of, https://github.com/rnheroes/react-native-pitchy.
|
|
6
|
+
|
|
7
|
+
This package uses Turbo Modules and is only compatible with the new architecture.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npm install @techoptio/react-native-live-pitch-detection
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
### iOS
|
|
16
|
+
|
|
17
|
+
For iOS, you'll need to add microphone permission to your `Info.plist`:
|
|
18
|
+
|
|
19
|
+
```xml
|
|
20
|
+
<key>NSMicrophoneUsageDescription</key>
|
|
21
|
+
<string>This app needs microphone access to detect pitch.</string>
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Android
|
|
25
|
+
|
|
26
|
+
For Android, you'll need to add microphone permission to your `AndroidManifest.xml`:
|
|
27
|
+
|
|
28
|
+
```xml
|
|
29
|
+
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
|
|
34
|
+
> **Important:** This library assumes microphone permissions have already been requested and granted before calling `startListening()`. Make sure to request permissions using a library like [`react-native-permissions`](https://github.com/zoontek/react-native-permissions) before using this library.
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
import PitchDetection from '@techoptio/react-native-live-pitch-detection';
|
|
38
|
+
import { request, PERMISSIONS, RESULTS } from 'react-native-permissions';
|
|
39
|
+
import { Platform } from 'react-native';
|
|
40
|
+
|
|
41
|
+
// Request microphone permission first
|
|
42
|
+
const permission = Platform.OS === 'ios'
|
|
43
|
+
? PERMISSIONS.IOS.MICROPHONE
|
|
44
|
+
: PERMISSIONS.ANDROID.RECORD_AUDIO;
|
|
45
|
+
|
|
46
|
+
const result = await request(permission);
|
|
47
|
+
|
|
48
|
+
if (result === RESULTS.GRANTED) {
|
|
49
|
+
// Configure options (optional) - should be called while not listening
|
|
50
|
+
PitchDetection.setOptions({
|
|
51
|
+
bufferSize: 4096, // Audio buffer size (default: 4096)
|
|
52
|
+
minVolume: -20.0, // Minimum volume threshold in dB (default: -20.0)
|
|
53
|
+
updateIntervalMs: 100, // Update interval in milliseconds (default: 100)
|
|
54
|
+
a4Frequency: 440, // A4 note frequency in Hz (default: 440)
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Start listening
|
|
58
|
+
await PitchDetection.startListening();
|
|
59
|
+
|
|
60
|
+
// Add listener for pitch events
|
|
61
|
+
const subscription = PitchDetection.addListener((event) => {
|
|
62
|
+
console.log('Frequency:', event.frequency);
|
|
63
|
+
console.log('Note:', event.note); // e.g., "C4", "A#3", etc.
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Stop listening when done
|
|
67
|
+
await PitchDetection.stopListening();
|
|
68
|
+
subscription.remove();
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## API
|
|
73
|
+
|
|
74
|
+
### Methods
|
|
75
|
+
|
|
76
|
+
#### `setOptions(options: Options)`
|
|
77
|
+
|
|
78
|
+
Configure the pitch detection settings. This method is optional, as all parameters have defaults. However, if you want to customize the settings, **call this method while not listening** (i.e., before `startListening()` or after `stopListening()`), otherwise all parameters may not be applied until the next listening session.
|
|
79
|
+
|
|
80
|
+
**Parameters:**
|
|
81
|
+
- `options.bufferSize?: number` - Audio buffer size (default: 4096)
|
|
82
|
+
- `options.minVolume?: number` - Minimum volume threshold in dB (default: -20.0)
|
|
83
|
+
- `options.updateIntervalMs?: number` - Update interval in milliseconds (default: 100)
|
|
84
|
+
- `options.a4Frequency?: number` - A4 note frequency in Hz (default: 440)
|
|
85
|
+
|
|
86
|
+
#### `startListening(): Promise<void>`
|
|
87
|
+
|
|
88
|
+
Starts listening to the microphone for pitch detection.
|
|
89
|
+
|
|
90
|
+
> **Note:** Ensure microphone permissions have been requested and granted before calling this method.
|
|
91
|
+
|
|
92
|
+
#### `stopListening(): Promise<void>`
|
|
93
|
+
|
|
94
|
+
Stops listening to the microphone.
|
|
95
|
+
|
|
96
|
+
#### `addListener(callback: (event: PitchEvent) => void): EventSubscription`
|
|
97
|
+
|
|
98
|
+
Adds a listener for pitch detection events. Returns an `EventSubscription` that should be removed when no longer needed.
|
|
99
|
+
|
|
100
|
+
**Event:**
|
|
101
|
+
```typescript
|
|
102
|
+
type PitchEvent = {
|
|
103
|
+
frequency: number; // Detected frequency in Hz
|
|
104
|
+
note: string; // Musical note (e.g., "C4", "A#3", "-" if no note detected)
|
|
105
|
+
};
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
#### `isListening(): boolean`
|
|
109
|
+
|
|
110
|
+
Returns `true` if currently listening, `false` otherwise.
|
|
111
|
+
|
|
112
|
+
## Types
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
type Options = {
|
|
116
|
+
bufferSize?: number;
|
|
117
|
+
minVolume?: number;
|
|
118
|
+
updateIntervalMs?: number;
|
|
119
|
+
a4Frequency?: number;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
type PitchEvent = {
|
|
123
|
+
frequency: number;
|
|
124
|
+
note: string;
|
|
125
|
+
};
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Example
|
|
129
|
+
|
|
130
|
+
Check out the [example app](example/) for a complete working implementation.
|
|
131
|
+
|
|
132
|
+

|
|
133
|
+
|
|
134
|
+
## Contributing
|
|
135
|
+
|
|
136
|
+
- [Development workflow](CONTRIBUTING.md#development-workflow)
|
|
137
|
+
- [Sending a pull request](CONTRIBUTING.md#sending-a-pull-request)
|
|
138
|
+
- [Code of conduct](CODE_OF_CONDUCT.md)
|
|
139
|
+
|
|
140
|
+
## License
|
|
141
|
+
|
|
142
|
+
MIT
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
Made with [create-react-native-library](https://github.com/callstack/react-native-builder-bob)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = "ReactNativeLivePitchDetection"
|
|
7
|
+
s.version = package["version"]
|
|
8
|
+
s.summary = package["description"]
|
|
9
|
+
s.homepage = package["homepage"]
|
|
10
|
+
s.license = package["license"]
|
|
11
|
+
s.authors = package["author"]
|
|
12
|
+
|
|
13
|
+
s.platforms = { :ios => min_ios_version_supported }
|
|
14
|
+
s.source = { :git => "https://github.com/techoptio/react-native-live-pitch-detection.git", :tag => "#{s.version}" }
|
|
15
|
+
|
|
16
|
+
s.source_files = "ios/**/*.{h,m,mm,cpp}", "shared/**/*.{hpp,cpp,c,h}"
|
|
17
|
+
s.private_header_files = "ios/**/*.h"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
install_modules_dependencies(s)
|
|
21
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
buildscript {
|
|
2
|
+
ext.getExtOrDefault = {name ->
|
|
3
|
+
return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties['ReactNativeLivePitchDetection_' + name]
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
repositories {
|
|
7
|
+
google()
|
|
8
|
+
mavenCentral()
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
dependencies {
|
|
12
|
+
classpath "com.android.tools.build:gradle:8.7.2"
|
|
13
|
+
// noinspection DifferentKotlinGradleVersion
|
|
14
|
+
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
apply plugin: "com.android.library"
|
|
20
|
+
apply plugin: "kotlin-android"
|
|
21
|
+
|
|
22
|
+
apply plugin: "com.facebook.react"
|
|
23
|
+
|
|
24
|
+
def getExtOrIntegerDefault(name) {
|
|
25
|
+
return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["ReactNativeLivePitchDetection_" + name]).toInteger()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
def reactNativeArchitectures() {
|
|
29
|
+
def value = rootProject.getProperties().get("reactNativeArchitectures")
|
|
30
|
+
return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
android {
|
|
34
|
+
namespace "io.techopt.lib.reactnativelivepitchdetection"
|
|
35
|
+
|
|
36
|
+
compileSdkVersion getExtOrIntegerDefault("compileSdkVersion")
|
|
37
|
+
|
|
38
|
+
defaultConfig {
|
|
39
|
+
minSdkVersion getExtOrIntegerDefault("minSdkVersion")
|
|
40
|
+
targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")
|
|
41
|
+
|
|
42
|
+
externalNativeBuild {
|
|
43
|
+
cmake {
|
|
44
|
+
cppFlags "-O2 -frtti -fexceptions -Wall -fstack-protector-all"
|
|
45
|
+
abiFilters (*reactNativeArchitectures())
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
buildFeatures {
|
|
51
|
+
buildConfig true
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
buildTypes {
|
|
55
|
+
release {
|
|
56
|
+
minifyEnabled false
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
externalNativeBuild {
|
|
61
|
+
cmake {
|
|
62
|
+
path "src/main/jni/CMakeLists.txt"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
lintOptions {
|
|
67
|
+
disable "GradleCompatible"
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
compileOptions {
|
|
71
|
+
sourceCompatibility JavaVersion.VERSION_1_8
|
|
72
|
+
targetCompatibility JavaVersion.VERSION_1_8
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
sourceSets {
|
|
76
|
+
main {
|
|
77
|
+
java.srcDirs += [
|
|
78
|
+
"generated/java",
|
|
79
|
+
"generated/jni"
|
|
80
|
+
]
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
repositories {
|
|
86
|
+
mavenCentral()
|
|
87
|
+
google()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
def kotlin_version = getExtOrDefault("kotlinVersion")
|
|
91
|
+
|
|
92
|
+
dependencies {
|
|
93
|
+
implementation "com.facebook.react:react-android"
|
|
94
|
+
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
|
95
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
package io.techopt.lib.reactnativelivepitchdetection
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
4
|
+
import com.facebook.react.module.annotations.ReactModule
|
|
5
|
+
import com.facebook.react.bridge.ReadableMap
|
|
6
|
+
import com.facebook.react.bridge.WritableMap
|
|
7
|
+
import com.facebook.react.bridge.Arguments
|
|
8
|
+
import com.facebook.react.modules.core.DeviceEventManagerModule
|
|
9
|
+
import android.media.AudioRecord
|
|
10
|
+
import android.media.AudioFormat
|
|
11
|
+
import android.media.MediaRecorder
|
|
12
|
+
import com.facebook.react.bridge.Promise
|
|
13
|
+
import kotlin.concurrent.thread
|
|
14
|
+
import android.util.Log
|
|
15
|
+
@ReactModule(name = ReactNativeLivePitchDetectionModule.NAME)
|
|
16
|
+
class ReactNativeLivePitchDetectionModule(reactContext: ReactApplicationContext) :
|
|
17
|
+
NativeReactNativeLivePitchDetectionSpec(reactContext) {
|
|
18
|
+
|
|
19
|
+
private var isListening = false
|
|
20
|
+
|
|
21
|
+
private var audioRecord: AudioRecord? = null
|
|
22
|
+
private var recordingThread: Thread? = null
|
|
23
|
+
|
|
24
|
+
private var sampleRate: Int = 44100
|
|
25
|
+
|
|
26
|
+
private var updateIntervalMs: Int = 100
|
|
27
|
+
|
|
28
|
+
private var minVolume: Double = -20.0
|
|
29
|
+
private var bufferSize: Int = 4096
|
|
30
|
+
|
|
31
|
+
override fun getName(): String {
|
|
32
|
+
return NAME
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
override fun isListening(): Boolean {
|
|
36
|
+
return isListening
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
override fun setOptions(bufferSize: Double, minVolume: Double, updateIntervalMs: Double) {
|
|
40
|
+
this.minVolume = minVolume
|
|
41
|
+
this.bufferSize = bufferSize.toInt()
|
|
42
|
+
this.updateIntervalMs = updateIntervalMs.toInt()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
override fun startListening(promise: Promise) {
|
|
46
|
+
|
|
47
|
+
if (isListening) {
|
|
48
|
+
promise.reject("E_ALREADY_LISTENING", "Already listening")
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
audioRecord = AudioRecord(MediaRecorder.AudioSource.MIC, sampleRate, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, bufferSize)
|
|
53
|
+
|
|
54
|
+
audioRecord?.startRecording()
|
|
55
|
+
|
|
56
|
+
recordingThread = thread(start = true) {
|
|
57
|
+
val buffer = ShortArray(bufferSize)
|
|
58
|
+
var lastUpdateTime = System.currentTimeMillis()
|
|
59
|
+
|
|
60
|
+
while (isListening) {
|
|
61
|
+
val currentTime = System.currentTimeMillis()
|
|
62
|
+
|
|
63
|
+
if (currentTime - lastUpdateTime >= updateIntervalMs) {
|
|
64
|
+
val read = audioRecord?.read(buffer, 0, bufferSize)
|
|
65
|
+
|
|
66
|
+
if (read != null && read > 0) {
|
|
67
|
+
detectPitch(buffer)
|
|
68
|
+
lastUpdateTime = currentTime
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
isListening = true
|
|
75
|
+
|
|
76
|
+
promise.resolve(null)
|
|
77
|
+
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
override fun stopListening(promise: Promise) {
|
|
81
|
+
|
|
82
|
+
if (!isListening) {
|
|
83
|
+
promise.reject("E_NOT_LISTENING", "Not listening")
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
isListening = false
|
|
88
|
+
audioRecord?.stop()
|
|
89
|
+
audioRecord?.release()
|
|
90
|
+
audioRecord = null
|
|
91
|
+
recordingThread?.interrupt()
|
|
92
|
+
recordingThread = null
|
|
93
|
+
promise.resolve(null)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private external fun nativeAutoCorrelate(buffer: ShortArray, sampleRate: Int, minVolume: Double): Double
|
|
97
|
+
|
|
98
|
+
private fun detectPitch(buffer: ShortArray) {
|
|
99
|
+
val frequency = nativeAutoCorrelate(buffer, sampleRate, minVolume)
|
|
100
|
+
|
|
101
|
+
val eventData = Arguments.createMap().apply {
|
|
102
|
+
putDouble("frequency", frequency)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
emitOnFrequencyDetected(eventData)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
companion object {
|
|
109
|
+
const val NAME = "ReactNativeLivePitchDetection"
|
|
110
|
+
|
|
111
|
+
init {
|
|
112
|
+
System.loadLibrary("reactnativelivepitchdetection")
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
package io.techopt.lib.reactnativelivepitchdetection
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.BaseReactPackage
|
|
4
|
+
import com.facebook.react.bridge.NativeModule
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.module.model.ReactModuleInfo
|
|
7
|
+
import com.facebook.react.module.model.ReactModuleInfoProvider
|
|
8
|
+
import java.util.HashMap
|
|
9
|
+
|
|
10
|
+
class ReactNativeLivePitchDetectionPackage : BaseReactPackage() {
|
|
11
|
+
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
|
|
12
|
+
return if (name == ReactNativeLivePitchDetectionModule.NAME) {
|
|
13
|
+
ReactNativeLivePitchDetectionModule(reactContext)
|
|
14
|
+
} else {
|
|
15
|
+
null
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
|
|
20
|
+
return ReactModuleInfoProvider {
|
|
21
|
+
val moduleInfos: MutableMap<String, ReactModuleInfo> = HashMap()
|
|
22
|
+
moduleInfos[ReactNativeLivePitchDetectionModule.NAME] = ReactModuleInfo(
|
|
23
|
+
ReactNativeLivePitchDetectionModule.NAME,
|
|
24
|
+
ReactNativeLivePitchDetectionModule.NAME,
|
|
25
|
+
false, // canOverrideExistingModule
|
|
26
|
+
false, // needsEagerInit
|
|
27
|
+
false, // isCxxModule
|
|
28
|
+
true // isTurboModule
|
|
29
|
+
)
|
|
30
|
+
moduleInfos
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
cmake_minimum_required(VERSION 3.4.1)
|
|
2
|
+
project(ReactNativeLivePitchDetection)
|
|
3
|
+
|
|
4
|
+
set (CMAKE_VERBOSE_MAKEFILE ON)
|
|
5
|
+
set (CMAKE_CXX_STANDARD 14)
|
|
6
|
+
|
|
7
|
+
add_library(reactnativelivepitchdetection SHARED
|
|
8
|
+
../../../../shared/ReactNativeLivePitchDetectionModule.cpp
|
|
9
|
+
cpp-adapter.cpp
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
# Specifies a path to native header files.
|
|
13
|
+
include_directories(
|
|
14
|
+
../../../../shared
|
|
15
|
+
)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Thanks to rnheroes/react-native-pitchy for the original implementation
|
|
2
|
+
// https://github.com/rnheroes/react-native-pitchy/blob/main/android/cpp-adapter.cpp
|
|
3
|
+
|
|
4
|
+
#include <jni.h>
|
|
5
|
+
#include "../../../../shared/ReactNativeLivePitchDetectionModule.h"
|
|
6
|
+
#include <vector>
|
|
7
|
+
#include <cmath>
|
|
8
|
+
#include <algorithm>
|
|
9
|
+
#include <numeric>
|
|
10
|
+
#include <limits>
|
|
11
|
+
|
|
12
|
+
extern "C" JNIEXPORT jdouble JNICALL
|
|
13
|
+
Java_io_techopt_lib_reactnativelivepitchdetection_ReactNativeLivePitchDetectionModule_nativeAutoCorrelate(JNIEnv *env, jobject thiz, jshortArray buffer, jint sampleRate, jdouble minVolume)
|
|
14
|
+
{
|
|
15
|
+
jshort *buf = env->GetShortArrayElements(buffer, 0);
|
|
16
|
+
jsize size = env->GetArrayLength(buffer);
|
|
17
|
+
std::vector<double> vec(buf, buf + size);
|
|
18
|
+
env->ReleaseShortArrayElements(buffer, buf, 0);
|
|
19
|
+
|
|
20
|
+
double result = techoptio::reactnativelivepitchdetection::autoCorrelate(vec, sampleRate, minVolume);
|
|
21
|
+
return result;
|
|
22
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#ifdef __cplusplus
|
|
2
|
+
#import "ReactNativeLivePitchDetectionModule.h"
|
|
3
|
+
#endif
|
|
4
|
+
|
|
5
|
+
#import <ReactNativeLivePitchDetectionSpec/ReactNativeLivePitchDetectionSpec.h>
|
|
6
|
+
|
|
7
|
+
@interface ReactNativeLivePitchDetection : NativeReactNativeLivePitchDetectionSpecBase <NativeReactNativeLivePitchDetectionSpec>
|
|
8
|
+
|
|
9
|
+
@end
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// ReactNativeLivePitchDetection.mm
|
|
2
|
+
|
|
3
|
+
#import "ReactNativeLivePitchDetection.h"
|
|
4
|
+
|
|
5
|
+
#import <AVFoundation/AVFoundation.h>
|
|
6
|
+
#import <QuartzCore/QuartzCore.h>
|
|
7
|
+
|
|
8
|
+
@implementation ReactNativeLivePitchDetection
|
|
9
|
+
AVAudioEngine *_audioEngine = nil;
|
|
10
|
+
double _sampleRate = 44100;
|
|
11
|
+
double _minVolume = -20.0;
|
|
12
|
+
AVAudioFrameCount _bufferSize = 4096;
|
|
13
|
+
int _updateIntervalMs = 100;
|
|
14
|
+
BOOL _isListening = NO;
|
|
15
|
+
NSTimeInterval _lastUpdateTime = 0;
|
|
16
|
+
|
|
17
|
+
- (void)setOptions:(double)bufferSize minVolume:(double)minVolume updateIntervalMs:(double)updateIntervalMs {
|
|
18
|
+
_minVolume = minVolume;
|
|
19
|
+
_updateIntervalMs = (int)updateIntervalMs;
|
|
20
|
+
_bufferSize = (AVAudioFrameCount)bufferSize;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
- (NSNumber *)isListening {
|
|
24
|
+
return @(_isListening);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
- (void)startListening:(RCTPromiseResolveBlock)resolve
|
|
28
|
+
reject:(RCTPromiseRejectBlock)reject {
|
|
29
|
+
|
|
30
|
+
#if TARGET_IPHONE_SIMULATOR
|
|
31
|
+
reject(@"E_NOT_SUPPORTED_ON_IOS_SIMULATOR", @"React Native Live Pitch Detection module is not supported on the iOS simulator. Please use a real device for testing.", nil);
|
|
32
|
+
return;
|
|
33
|
+
#endif
|
|
34
|
+
|
|
35
|
+
if (_isListening) {
|
|
36
|
+
reject(@"E_ALREADY_LISTENING", @"Already listening", nil);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
NSError *error = nil;
|
|
41
|
+
|
|
42
|
+
if (!_audioEngine) {
|
|
43
|
+
_audioEngine = [[AVAudioEngine alloc] init];
|
|
44
|
+
|
|
45
|
+
AVAudioInputNode *inputNode = [_audioEngine inputNode];
|
|
46
|
+
|
|
47
|
+
AVAudioFormat *format = [inputNode inputFormatForBus:0];
|
|
48
|
+
_sampleRate = format.sampleRate;
|
|
49
|
+
|
|
50
|
+
[inputNode installTapOnBus:0 bufferSize:_bufferSize format:format block:^(AVAudioPCMBuffer * _Nonnull buffer, AVAudioTime * _Nonnull when) {
|
|
51
|
+
[self detectPitch:buffer];
|
|
52
|
+
}];
|
|
53
|
+
|
|
54
|
+
AVAudioSession *session = [AVAudioSession sharedInstance];
|
|
55
|
+
|
|
56
|
+
[session setCategory:AVAudioSessionCategoryPlayAndRecord
|
|
57
|
+
mode:AVAudioSessionModeMeasurement
|
|
58
|
+
options:AVAudioSessionCategoryOptionDefaultToSpeaker
|
|
59
|
+
error:&error];
|
|
60
|
+
|
|
61
|
+
if (error) {
|
|
62
|
+
reject(@"E_SET_CATEGORY_FAILED", @"Failed to set audio session category", error);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
[_audioEngine startAndReturnError:&error];
|
|
68
|
+
|
|
69
|
+
if (error) {
|
|
70
|
+
reject(@"E_SET_ACTIVE_FAILED", @"Failed to set audio engine active", error);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
_isListening = YES;
|
|
75
|
+
|
|
76
|
+
resolve(nil);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
- (void)stopListening:(RCTPromiseResolveBlock)resolve
|
|
80
|
+
reject:(RCTPromiseRejectBlock)reject {
|
|
81
|
+
|
|
82
|
+
if (!_isListening) {
|
|
83
|
+
reject(@"E_NOT_LISTENING", @"Not listening", nil);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!_audioEngine) {
|
|
88
|
+
reject(@"E_NOT_INITIALIZED", @"Not initialized", nil);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
[_audioEngine stop];
|
|
93
|
+
|
|
94
|
+
_isListening = NO;
|
|
95
|
+
_lastUpdateTime = 0;
|
|
96
|
+
resolve(nil);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
|
|
100
|
+
(const facebook::react::ObjCTurboModule::InitParams &)params
|
|
101
|
+
{
|
|
102
|
+
return std::make_shared<facebook::react::NativeReactNativeLivePitchDetectionSpecJSI>(params);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
- (void)detectPitch:(AVAudioPCMBuffer *)buffer {
|
|
106
|
+
NSTimeInterval currentTime = CACurrentMediaTime();
|
|
107
|
+
NSTimeInterval intervalSeconds = _updateIntervalMs / 1000.0;
|
|
108
|
+
|
|
109
|
+
if (_lastUpdateTime == 0 || (currentTime - _lastUpdateTime) >= intervalSeconds) {
|
|
110
|
+
float *channelData = buffer.floatChannelData[0];
|
|
111
|
+
std::vector<double> buf(channelData, channelData + buffer.frameLength);
|
|
112
|
+
|
|
113
|
+
double frequency = techoptio::reactnativelivepitchdetection::autoCorrelate(buf, _sampleRate, _minVolume);
|
|
114
|
+
|
|
115
|
+
[self emitOnFrequencyDetected:@{@"frequency": @(frequency)}];
|
|
116
|
+
|
|
117
|
+
_lastUpdateTime = currentTime;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
+ (NSString *)moduleName
|
|
122
|
+
{
|
|
123
|
+
return @"ReactNativeLivePitchDetection";
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
@end
|
|
127
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["TurboModuleRegistry","getEnforcing"],"sourceRoot":"../../src","sources":["NativeReactNativeLivePitchDetection.ts"],"mappings":";;AAAA,SAASA,mBAAmB,QAA6C,cAAc;AAUvF,eAAeA,mBAAmB,CAACC,YAAY,CAAO,+BAA+B,CAAC","ignoreList":[]}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import NativeReactNativeLivePitchDetection from "./NativeReactNativeLivePitchDetection.js";
|
|
4
|
+
let A4 = 440; // A4 note frequency
|
|
5
|
+
const noteNames = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
|
|
6
|
+
const ReactNativeLivePitchDetection = {
|
|
7
|
+
setOptions: options => {
|
|
8
|
+
A4 = options?.a4Frequency ?? 440;
|
|
9
|
+
NativeReactNativeLivePitchDetection.setOptions(options?.bufferSize ?? 4096, options?.minVolume ?? -20.0, options?.updateIntervalMs ?? 100);
|
|
10
|
+
},
|
|
11
|
+
startListening: () => {
|
|
12
|
+
return NativeReactNativeLivePitchDetection.startListening();
|
|
13
|
+
},
|
|
14
|
+
stopListening: () => {
|
|
15
|
+
return NativeReactNativeLivePitchDetection.stopListening();
|
|
16
|
+
},
|
|
17
|
+
addListener: callback => {
|
|
18
|
+
return NativeReactNativeLivePitchDetection.onFrequencyDetected(event => {
|
|
19
|
+
callback({
|
|
20
|
+
frequency: event.frequency,
|
|
21
|
+
note: getNoteFromFrequency(event.frequency)
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
},
|
|
25
|
+
isListening: () => {
|
|
26
|
+
return NativeReactNativeLivePitchDetection.isListening();
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
function getNoteFromFrequency(frequency) {
|
|
30
|
+
if (frequency <= 0 || !isFinite(frequency)) return "-";
|
|
31
|
+
const noteNumber = 12 * Math.log2(frequency / A4) + 69;
|
|
32
|
+
const noteIndex = Math.round(noteNumber) % 12;
|
|
33
|
+
const octave = Math.floor(Math.round(noteNumber) / 12) - 1;
|
|
34
|
+
return `${noteNames[noteIndex]}${octave}`;
|
|
35
|
+
}
|
|
36
|
+
;
|
|
37
|
+
export default ReactNativeLivePitchDetection;
|
|
38
|
+
export * from "./types.js";
|
|
39
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["NativeReactNativeLivePitchDetection","A4","noteNames","ReactNativeLivePitchDetection","setOptions","options","a4Frequency","bufferSize","minVolume","updateIntervalMs","startListening","stopListening","addListener","callback","onFrequencyDetected","event","frequency","note","getNoteFromFrequency","isListening","isFinite","noteNumber","Math","log2","noteIndex","round","octave","floor"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":";;AAAA,OAAOA,mCAAmC,MAAM,0CAAuC;AAMvF,IAAIC,EAAE,GAAG,GAAG,CAAC,CAAC;AACd,MAAMC,SAAS,GAAG,CAChB,GAAG,EACH,IAAI,EACJ,GAAG,EACH,IAAI,EACJ,GAAG,EACH,GAAG,EACH,IAAI,EACJ,GAAG,EACH,IAAI,EACJ,GAAG,EACH,IAAI,EACJ,GAAG,CACJ;AAED,MAAMC,6BAA6B,GAAG;EACpCC,UAAU,EAAGC,OAAgB,IAAK;IAChCJ,EAAE,GAAGI,OAAO,EAAEC,WAAW,IAAI,GAAG;IAChCN,mCAAmC,CAACI,UAAU,CAC5CC,OAAO,EAAEE,UAAU,IAAI,IAAI,EAC3BF,OAAO,EAAEG,SAAS,IAAI,CAAC,IAAI,EAC3BH,OAAO,EAAEI,gBAAgB,IAAI,GAC/B,CAAC;EACH,CAAC;EACDC,cAAc,EAAEA,CAAA,KAAM;IACpB,OAAOV,mCAAmC,CAACU,cAAc,CAAC,CAAC;EAC7D,CAAC;EACDC,aAAa,EAAEA,CAAA,KAAM;IACnB,OAAOX,mCAAmC,CAACW,aAAa,CAAC,CAAC;EAC5D,CAAC;EACDC,WAAW,EAAGC,QAAoD,IAAK;IACrE,OAAOb,mCAAmC,CAACc,mBAAmB,CAAEC,KAAK,IAAK;MACxEF,QAAQ,CAAC;QACPG,SAAS,EAAED,KAAK,CAACC,SAAS;QAC1BC,IAAI,EAAEC,oBAAoB,CAACH,KAAK,CAACC,SAAS;MAC5C,CAAC,CAAC;IACJ,CAAC,CAAC;EACJ,CAAC;EACDG,WAAW,EAAEA,CAAA,KAAM;IACjB,OAAOnB,mCAAmC,CAACmB,WAAW,CAAC,CAAC;EAC1D;AACF,CAAC;AAED,SAASD,oBAAoBA,CAACF,SAAiB,EAAU;EACvD,IAAIA,SAAS,IAAI,CAAC,IAAI,CAACI,QAAQ,CAACJ,SAAS,CAAC,EAAE,OAAO,GAAG;EAEtD,MAAMK,UAAU,GAAG,EAAE,GAAGC,IAAI,CAACC,IAAI,CAACP,SAAS,GAAGf,EAAE,CAAC,GAAG,EAAE;EACtD,MAAMuB,SAAS,GAAGF,IAAI,CAACG,KAAK,CAACJ,UAAU,CAAC,GAAG,EAAE;EAC7C,MAAMK,MAAM,GAAGJ,IAAI,CAACK,KAAK,CAACL,IAAI,CAACG,KAAK,CAACJ,UAAU,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC;EAE1D,OAAO,GAAGnB,SAAS,CAACsB,SAAS,CAAC,GAAGE,MAAM,EAAE;AAC3C;AAAC;AAGD,eAAevB,6BAA6B;AAE5C,cAAc,YAAS","ignoreList":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"module"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":[],"sourceRoot":"../../src","sources":["types.ts"],"mappings":"","ignoreList":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"module"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type TurboModule, type CodegenTypes } from 'react-native';
|
|
2
|
+
export interface Spec extends TurboModule {
|
|
3
|
+
setOptions(bufferSize: number, minVolume: number, updateIntervalMs: number): void;
|
|
4
|
+
startListening(): Promise<void>;
|
|
5
|
+
stopListening(): Promise<void>;
|
|
6
|
+
isListening(): boolean;
|
|
7
|
+
readonly onFrequencyDetected: CodegenTypes.EventEmitter<{
|
|
8
|
+
frequency: number;
|
|
9
|
+
}>;
|
|
10
|
+
}
|
|
11
|
+
declare const _default: Spec;
|
|
12
|
+
export default _default;
|
|
13
|
+
//# sourceMappingURL=NativeReactNativeLivePitchDetection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NativeReactNativeLivePitchDetection.d.ts","sourceRoot":"","sources":["../../../src/NativeReactNativeLivePitchDetection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuB,KAAK,WAAW,EAAE,KAAK,YAAY,EAAE,MAAM,cAAc,CAAC;AAExF,MAAM,WAAW,IAAK,SAAQ,WAAW;IACvC,UAAU,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClF,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAChC,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/B,WAAW,IAAI,OAAO,CAAC;IACvB,QAAQ,CAAC,mBAAmB,EAAE,YAAY,CAAC,YAAY,CAAC;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAChF;;AAED,wBAAuF"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Options, ReactNativeLivePitchDetectionEventCallback } from './types';
|
|
2
|
+
declare const ReactNativeLivePitchDetection: {
|
|
3
|
+
setOptions: (options: Options) => void;
|
|
4
|
+
startListening: () => Promise<void>;
|
|
5
|
+
stopListening: () => Promise<void>;
|
|
6
|
+
addListener: (callback: ReactNativeLivePitchDetectionEventCallback) => import("react-native").EventSubscription;
|
|
7
|
+
isListening: () => boolean;
|
|
8
|
+
};
|
|
9
|
+
export default ReactNativeLivePitchDetection;
|
|
10
|
+
export * from './types';
|
|
11
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,OAAO,EACP,0CAA0C,EAC3C,MAAM,SAAS,CAAC;AAkBjB,QAAA,MAAM,6BAA6B;0BACX,OAAO;;;4BAcL,0CAA0C;;CAWnE,CAAC;AAaF,eAAe,6BAA6B,CAAC;AAE7C,cAAc,SAAS,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type PitchEvent = {
|
|
2
|
+
frequency: number;
|
|
3
|
+
note: string;
|
|
4
|
+
};
|
|
5
|
+
export type ReactNativeLivePitchDetectionEventCallback = (event: PitchEvent) => void;
|
|
6
|
+
export type Options = {
|
|
7
|
+
bufferSize?: number;
|
|
8
|
+
minVolume?: number;
|
|
9
|
+
updateIntervalMs?: number;
|
|
10
|
+
a4Frequency?: number;
|
|
11
|
+
};
|
|
12
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,UAAU,GAAG;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,0CAA0C,GAAG,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;AAErF,MAAM,MAAM,OAAO,GAAG;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAC;CACxB,CAAA"}
|
package/package.json
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@techoptio/react-native-live-pitch-detection",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Live pitch detection (frequency, note and octave) from device microphone for React Native.",
|
|
5
|
+
"main": "./lib/module/index.js",
|
|
6
|
+
"types": "./lib/typescript/src/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"source": "./src/index.tsx",
|
|
10
|
+
"types": "./lib/typescript/src/index.d.ts",
|
|
11
|
+
"default": "./lib/module/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./package.json": "./package.json"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"src",
|
|
17
|
+
"lib",
|
|
18
|
+
"android",
|
|
19
|
+
"ios",
|
|
20
|
+
"cpp",
|
|
21
|
+
"*.podspec",
|
|
22
|
+
"react-native.config.js",
|
|
23
|
+
"!ios/build",
|
|
24
|
+
"!android/build",
|
|
25
|
+
"!android/gradle",
|
|
26
|
+
"!android/gradlew",
|
|
27
|
+
"!android/gradlew.bat",
|
|
28
|
+
"!android/local.properties",
|
|
29
|
+
"!**/__tests__",
|
|
30
|
+
"!**/__fixtures__",
|
|
31
|
+
"!**/__mocks__",
|
|
32
|
+
"!**/.*"
|
|
33
|
+
],
|
|
34
|
+
"scripts": {
|
|
35
|
+
"example": "yarn workspace @techoptio/react-native-live-pitch-detection-example",
|
|
36
|
+
"test": "jest",
|
|
37
|
+
"typecheck": "tsc",
|
|
38
|
+
"lint": "eslint \"**/*.{js,ts,tsx}\"",
|
|
39
|
+
"clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib",
|
|
40
|
+
"prepare": "bob build",
|
|
41
|
+
"release": "release-it --only-version"
|
|
42
|
+
},
|
|
43
|
+
"keywords": [
|
|
44
|
+
"react-native",
|
|
45
|
+
"ios",
|
|
46
|
+
"android"
|
|
47
|
+
],
|
|
48
|
+
"repository": {
|
|
49
|
+
"type": "git",
|
|
50
|
+
"url": "git+https://github.com/techoptio/react-native-live-pitch-detection.git"
|
|
51
|
+
},
|
|
52
|
+
"author": "TechOpt.io <oss@techopt.io> (https://github.com/techoptio)",
|
|
53
|
+
"license": "MIT",
|
|
54
|
+
"bugs": {
|
|
55
|
+
"url": "https://github.com/techoptio/react-native-live-pitch-detection/issues"
|
|
56
|
+
},
|
|
57
|
+
"homepage": "https://github.com/techoptio/react-native-live-pitch-detection#readme",
|
|
58
|
+
"publishConfig": {
|
|
59
|
+
"registry": "https://registry.npmjs.org/"
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"@commitlint/config-conventional": "^19.8.1",
|
|
63
|
+
"@eslint/compat": "^1.3.2",
|
|
64
|
+
"@eslint/eslintrc": "^3.3.1",
|
|
65
|
+
"@eslint/js": "^9.35.0",
|
|
66
|
+
"@evilmartians/lefthook": "^1.12.3",
|
|
67
|
+
"@react-native-community/cli": "20.0.1",
|
|
68
|
+
"@react-native/babel-preset": "0.81.1",
|
|
69
|
+
"@react-native/eslint-config": "^0.81.1",
|
|
70
|
+
"@release-it/conventional-changelog": "^10.0.1",
|
|
71
|
+
"@types/jest": "^29.5.14",
|
|
72
|
+
"@types/react": "^19.1.0",
|
|
73
|
+
"commitlint": "^19.8.1",
|
|
74
|
+
"del-cli": "^6.0.0",
|
|
75
|
+
"eslint": "^9.35.0",
|
|
76
|
+
"eslint-config-prettier": "^10.1.8",
|
|
77
|
+
"eslint-plugin-prettier": "^5.5.4",
|
|
78
|
+
"jest": "^29.7.0",
|
|
79
|
+
"prettier": "^3.6.2",
|
|
80
|
+
"react": "19.1.0",
|
|
81
|
+
"react-native": "0.81.1",
|
|
82
|
+
"react-native-builder-bob": "^0.40.14",
|
|
83
|
+
"release-it": "^19.0.4",
|
|
84
|
+
"turbo": "^2.5.6",
|
|
85
|
+
"typescript": "^5.9.2"
|
|
86
|
+
},
|
|
87
|
+
"peerDependencies": {
|
|
88
|
+
"react": "*",
|
|
89
|
+
"react-native": "*"
|
|
90
|
+
},
|
|
91
|
+
"workspaces": [
|
|
92
|
+
"example"
|
|
93
|
+
],
|
|
94
|
+
"packageManager": "yarn@3.6.1",
|
|
95
|
+
"jest": {
|
|
96
|
+
"preset": "react-native",
|
|
97
|
+
"modulePathIgnorePatterns": [
|
|
98
|
+
"<rootDir>/example/node_modules",
|
|
99
|
+
"<rootDir>/lib/"
|
|
100
|
+
]
|
|
101
|
+
},
|
|
102
|
+
"commitlint": {
|
|
103
|
+
"extends": [
|
|
104
|
+
"@commitlint/config-conventional"
|
|
105
|
+
]
|
|
106
|
+
},
|
|
107
|
+
"release-it": {
|
|
108
|
+
"git": {
|
|
109
|
+
"commitMessage": "chore: release ${version}",
|
|
110
|
+
"tagName": "v${version}"
|
|
111
|
+
},
|
|
112
|
+
"npm": {
|
|
113
|
+
"publish": true
|
|
114
|
+
},
|
|
115
|
+
"github": {
|
|
116
|
+
"release": true
|
|
117
|
+
},
|
|
118
|
+
"plugins": {
|
|
119
|
+
"@release-it/conventional-changelog": {
|
|
120
|
+
"preset": {
|
|
121
|
+
"name": "angular"
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
"prettier": {
|
|
127
|
+
"quoteProps": "consistent",
|
|
128
|
+
"singleQuote": true,
|
|
129
|
+
"tabWidth": 2,
|
|
130
|
+
"trailingComma": "es5",
|
|
131
|
+
"useTabs": false
|
|
132
|
+
},
|
|
133
|
+
"react-native-builder-bob": {
|
|
134
|
+
"source": "src",
|
|
135
|
+
"output": "lib",
|
|
136
|
+
"targets": [
|
|
137
|
+
[
|
|
138
|
+
"module",
|
|
139
|
+
{
|
|
140
|
+
"esm": true
|
|
141
|
+
}
|
|
142
|
+
],
|
|
143
|
+
[
|
|
144
|
+
"typescript",
|
|
145
|
+
{
|
|
146
|
+
"project": "tsconfig.build.json"
|
|
147
|
+
}
|
|
148
|
+
]
|
|
149
|
+
]
|
|
150
|
+
},
|
|
151
|
+
"codegenConfig": {
|
|
152
|
+
"name": "ReactNativeLivePitchDetectionSpec",
|
|
153
|
+
"type": "modules",
|
|
154
|
+
"jsSrcsDir": "src",
|
|
155
|
+
"android": {
|
|
156
|
+
"javaPackageName": "io.techopt.lib.reactnativelivepitchdetection"
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
"create-react-native-library": {
|
|
160
|
+
"languages": "kotlin-objc",
|
|
161
|
+
"type": "turbo-module",
|
|
162
|
+
"version": "0.54.8"
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { TurboModuleRegistry, type TurboModule, type CodegenTypes } from 'react-native';
|
|
2
|
+
|
|
3
|
+
export interface Spec extends TurboModule {
|
|
4
|
+
setOptions(bufferSize: number, minVolume: number, updateIntervalMs: number): void;
|
|
5
|
+
startListening(): Promise<void>;
|
|
6
|
+
stopListening(): Promise<void>;
|
|
7
|
+
isListening(): boolean;
|
|
8
|
+
readonly onFrequencyDetected: CodegenTypes.EventEmitter<{ frequency: number }>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default TurboModuleRegistry.getEnforcing<Spec>('ReactNativeLivePitchDetection');
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import NativeReactNativeLivePitchDetection from './NativeReactNativeLivePitchDetection';
|
|
2
|
+
import type {
|
|
3
|
+
Options,
|
|
4
|
+
ReactNativeLivePitchDetectionEventCallback,
|
|
5
|
+
} from './types';
|
|
6
|
+
|
|
7
|
+
let A4 = 440; // A4 note frequency
|
|
8
|
+
const noteNames = [
|
|
9
|
+
"C",
|
|
10
|
+
"C#",
|
|
11
|
+
"D",
|
|
12
|
+
"D#",
|
|
13
|
+
"E",
|
|
14
|
+
"F",
|
|
15
|
+
"F#",
|
|
16
|
+
"G",
|
|
17
|
+
"G#",
|
|
18
|
+
"A",
|
|
19
|
+
"A#",
|
|
20
|
+
"B",
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const ReactNativeLivePitchDetection = {
|
|
24
|
+
setOptions: (options: Options) => {
|
|
25
|
+
A4 = options?.a4Frequency ?? 440;
|
|
26
|
+
NativeReactNativeLivePitchDetection.setOptions(
|
|
27
|
+
options?.bufferSize ?? 4096,
|
|
28
|
+
options?.minVolume ?? -20.0,
|
|
29
|
+
options?.updateIntervalMs ?? 100
|
|
30
|
+
);
|
|
31
|
+
},
|
|
32
|
+
startListening: () => {
|
|
33
|
+
return NativeReactNativeLivePitchDetection.startListening();
|
|
34
|
+
},
|
|
35
|
+
stopListening: () => {
|
|
36
|
+
return NativeReactNativeLivePitchDetection.stopListening();
|
|
37
|
+
},
|
|
38
|
+
addListener: (callback: ReactNativeLivePitchDetectionEventCallback) => {
|
|
39
|
+
return NativeReactNativeLivePitchDetection.onFrequencyDetected((event) => {
|
|
40
|
+
callback({
|
|
41
|
+
frequency: event.frequency,
|
|
42
|
+
note: getNoteFromFrequency(event.frequency),
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
},
|
|
46
|
+
isListening: () => {
|
|
47
|
+
return NativeReactNativeLivePitchDetection.isListening();
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
function getNoteFromFrequency(frequency: number): string {
|
|
52
|
+
if (frequency <= 0 || !isFinite(frequency)) return "-";
|
|
53
|
+
|
|
54
|
+
const noteNumber = 12 * Math.log2(frequency / A4) + 69;
|
|
55
|
+
const noteIndex = Math.round(noteNumber) % 12;
|
|
56
|
+
const octave = Math.floor(Math.round(noteNumber) / 12) - 1;
|
|
57
|
+
|
|
58
|
+
return `${noteNames[noteIndex]}${octave}`;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
export default ReactNativeLivePitchDetection;
|
|
63
|
+
|
|
64
|
+
export * from './types';
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type PitchEvent = {
|
|
2
|
+
frequency: number;
|
|
3
|
+
note: string;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export type ReactNativeLivePitchDetectionEventCallback = (event: PitchEvent) => void;
|
|
7
|
+
|
|
8
|
+
export type Options = {
|
|
9
|
+
bufferSize?: number;
|
|
10
|
+
minVolume?: number;
|
|
11
|
+
updateIntervalMs?: number;
|
|
12
|
+
a4Frequency?: number;
|
|
13
|
+
}
|