@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.
Files changed (30) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +146 -0
  3. package/ReactNativeLivePitchDetection.podspec +21 -0
  4. package/android/build.gradle +95 -0
  5. package/android/gradle.properties +5 -0
  6. package/android/src/main/AndroidManifest.xml +3 -0
  7. package/android/src/main/java/io/techopt/lib/reactnativelivepitchdetection/ReactNativeLivePitchDetectionModule.kt +115 -0
  8. package/android/src/main/java/io/techopt/lib/reactnativelivepitchdetection/ReactNativeLivePitchDetectionPackage.kt +33 -0
  9. package/android/src/main/jni/CMakeLists.txt +15 -0
  10. package/android/src/main/jni/cpp-adapter.cpp +22 -0
  11. package/ios/ReactNativeLivePitchDetection.h +9 -0
  12. package/ios/ReactNativeLivePitchDetection.mm +127 -0
  13. package/lib/module/NativeReactNativeLivePitchDetection.js +5 -0
  14. package/lib/module/NativeReactNativeLivePitchDetection.js.map +1 -0
  15. package/lib/module/index.js +39 -0
  16. package/lib/module/index.js.map +1 -0
  17. package/lib/module/package.json +1 -0
  18. package/lib/module/types.js +2 -0
  19. package/lib/module/types.js.map +1 -0
  20. package/lib/typescript/package.json +1 -0
  21. package/lib/typescript/src/NativeReactNativeLivePitchDetection.d.ts +13 -0
  22. package/lib/typescript/src/NativeReactNativeLivePitchDetection.d.ts.map +1 -0
  23. package/lib/typescript/src/index.d.ts +11 -0
  24. package/lib/typescript/src/index.d.ts.map +1 -0
  25. package/lib/typescript/src/types.d.ts +12 -0
  26. package/lib/typescript/src/types.d.ts.map +1 -0
  27. package/package.json +164 -0
  28. package/src/NativeReactNativeLivePitchDetection.ts +11 -0
  29. package/src/index.tsx +64 -0
  30. 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
+ ![Example App Preview](example/preview.png)
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,5 @@
1
+ ReactNativeLivePitchDetection_kotlinVersion=2.0.21
2
+ ReactNativeLivePitchDetection_minSdkVersion=24
3
+ ReactNativeLivePitchDetection_targetSdkVersion=34
4
+ ReactNativeLivePitchDetection_compileSdkVersion=35
5
+ ReactNativeLivePitchDetection_ndkVersion=27.1.12297006
@@ -0,0 +1,3 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ <uses-permission android:name="android.permission.RECORD_AUDIO" />
3
+ </manifest>
@@ -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,5 @@
1
+ "use strict";
2
+
3
+ import { TurboModuleRegistry } from 'react-native';
4
+ export default TurboModuleRegistry.getEnforcing('ReactNativeLivePitchDetection');
5
+ //# sourceMappingURL=NativeReactNativeLivePitchDetection.js.map
@@ -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,2 @@
1
+ "use strict";
2
+ //# sourceMappingURL=types.js.map
@@ -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
+ }