Skip to content

Commit

Permalink
refactor: add audio listener, update api
Browse files Browse the repository at this point in the history
  • Loading branch information
yigithanyucedag committed Jul 28, 2024
1 parent 6415490 commit 52b24b5
Show file tree
Hide file tree
Showing 9 changed files with 245 additions and 96 deletions.
25 changes: 10 additions & 15 deletions android/cpp-adapter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,16 @@
#include <limits>

extern "C" JNIEXPORT jdouble JNICALL
Java_com_pitchy_PitchyModule_nativeAutoCorrelate(JNIEnv *env, jobject /* this */, jdoubleArray buf, jdouble sampleRate)
Java_com_pitchy_PitchyModule_nativeAutoCorrelate(JNIEnv *env, jobject thiz, jshortArray buffer, jint sampleRate, jdouble minVolume)
{
jsize len = env->GetArrayLength(buf);
jdouble *bufArray = env->GetDoubleArrayElements(buf, 0);
std::vector<double> vec(bufArray, bufArray + len);
env->ReleaseDoubleArrayElements(buf, bufArray, 0);
return pitchy::autoCorrelate(vec, sampleRate);
}
// pitchy::autoCorrelate(const std::vector<double> &buf, double sampleRate, double minVolume)
// Convert jshortArray to std::vector<double>
jshort *buf = env->GetShortArrayElements(buffer, 0);
jsize size = env->GetArrayLength(buffer);
std::vector<double> vec(buf, buf + size);
env->ReleaseShortArrayElements(buffer, buf, 0);

extern "C" JNIEXPORT jdouble JNICALL
Java_com_pitchy_PitchyModule_nativeCalculateVolume(JNIEnv *env, jobject /* this */, jdoubleArray buf)
{
jsize len = env->GetArrayLength(buf);
jdouble *bufArray = env->GetDoubleArrayElements(buf, 0);
std::vector<double> vec(bufArray, bufArray + len);
env->ReleaseDoubleArrayElements(buf, bufArray, 0);
return pitchy::calculateVolume(vec);
// Call pitchy::autoCorrelate
double result = pitchy::autoCorrelate(vec, sampleRate, minVolume);
return result;
}
4 changes: 2 additions & 2 deletions android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.pitchy">
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.pitchy">
<uses-permission android:name="android.permission.RECORD_AUDIO" />
</manifest>
1 change: 1 addition & 0 deletions android/src/main/AndroidManifestNew.xml
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.RECORD_AUDIO" />
</manifest>
111 changes: 100 additions & 11 deletions android/src/main/java/com/pitchy/PitchyModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,120 @@ import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.WritableMap
import com.facebook.react.modules.core.DeviceEventManagerModule

import android.media.AudioRecord
import android.media.AudioFormat
import android.media.MediaRecorder

import kotlin.concurrent.thread

class PitchyModule(reactContext: ReactApplicationContext) :
ReactContextBaseJavaModule(reactContext) {

private var isRecording = false
private var isInitialized = false

private var audioRecord: AudioRecord? = null
private var recordingThread: Thread? = null

private var sampleRate: Int = 44100

private var minVolume: Double = 0.0
private var bufferSize: Int = 0

override fun getName(): String {
return NAME
}

@ReactMethod
fun autoCorrelate(buf: ReadableArray, sampleRate: Double, promise: Promise) {
val bufArray = buf.toArrayList().map { it as Double }.toDoubleArray()
val pitch = nativeAutoCorrelate(bufArray, sampleRate)
promise.resolve(pitch)
fun init(config: ReadableMap) {
minVolume = config.getDouble("minVolume")
bufferSize = config.getInt("bufferSize")

audioRecord = AudioRecord(MediaRecorder.AudioSource.MIC, sampleRate, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, bufferSize)

isInitialized = true
}

@ReactMethod
fun isRecording(promise: Promise) {
promise.resolve(isRecording)
}

@ReactMethod
fun calculateVolume(buf: ReadableArray, promise: Promise) {
val bufArray = buf.toArrayList().map { it as Double }.toDoubleArray()
val volume = nativeCalculateVolume(bufArray)
promise.resolve(volume)
fun addListener(eventName: String) {
// Keep: Required for RN built in Event Emitter Calls.
}

private external fun nativeAutoCorrelate(buf: DoubleArray, sampleRate: Double): Double
private external fun nativeCalculateVolume(buf: DoubleArray): Double
@ReactMethod
fun removeListeners(count: Int) {
// Keep: Required for RN built in Event Emitter Calls.
}

@ReactMethod
fun start(promise: Promise) {

if(!isInitialized) {
promise.reject("E_NOT_INITIALIZED", "Not initialized")
return
}

if (isRecording) {
promise.reject("E_ALREADY_RECORDING", "Already recording")
return
}

startRecording()
promise.resolve(true)
}

@ReactMethod
fun stop(promise: Promise) {
if (!isRecording) {
promise.reject("E_NOT_RECORDING", "Not recording")
return
}

stopRecording()
promise.resolve(true)
}

private fun startRecording(){
audioRecord?.startRecording()
isRecording = true

recordingThread = thread(start = true) {
val buffer = ShortArray(bufferSize)
while (isRecording) {
val read = audioRecord?.read(buffer, 0, bufferSize)
if (read != null && read > 0) {
detectPitch(buffer)
}
}
}
}

private fun stopRecording() {
isRecording = false
audioRecord?.stop()
audioRecord?.release()
audioRecord = null
recordingThread?.interrupt()
recordingThread = null
}

private external fun nativeAutoCorrelate(buffer: ShortArray, sampleRate: Int, minVolume: Double): Double

private fun detectPitch(buffer: ShortArray){
val pitch = nativeAutoCorrelate(buffer, sampleRate, minVolume)
val params: WritableMap = Arguments.createMap()
params.putDouble("pitch", pitch)
reactApplicationContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java).emit("onPitchDetected", params)
}

companion object {
const val NAME = "Pitchy"
Expand Down
23 changes: 8 additions & 15 deletions cpp/react-native-pitchy.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,13 @@

namespace pitchy
{
double autoCorrelate(const std::vector<double> &buf, double sampleRate)
double getVolumeDecibel(double rms)
{
return 20 * std::log10(rms);
}

double autoCorrelate(const std::vector<double> &buf, double sampleRate, double minVolume)
{
// Implements the ACF2+ algorithm
int SIZE = buf.size();
double rms = 0;

Expand All @@ -20,7 +24,8 @@ namespace pitchy
rms += val * val;
}
rms = std::sqrt(rms / SIZE);
if (rms < 0.01) // not enough signal
double decibel = getVolumeDecibel(rms);
if (decibel < minVolume)
return -1;

int r1 = 0, r2 = SIZE - 1;
Expand Down Expand Up @@ -69,16 +74,4 @@ namespace pitchy
return sampleRate / T0;
}

double calculateVolume(const std::vector<double> &buf)
{
// Calculate RMS (Root Mean Square)
double sumSquares = std::accumulate(buf.begin(), buf.end(), 0.0, [](double a, double b)
{ return a + b * b; });
double rms = std::sqrt(sumSquares / buf.size());

// Convert RMS to decibels
double decibels = 20 * std::log10(rms);

return decibels;
}
}
3 changes: 1 addition & 2 deletions cpp/react-native-pitchy.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@

namespace pitchy
{
double autoCorrelate(const std::vector<double> &buf, double sampleRate);
double calculateVolume(const std::vector<double> &buf);
double autoCorrelate(const std::vector<double> &buf, double sampleRate, double minVolume);
}

#endif /* PITCHY_H */
6 changes: 4 additions & 2 deletions ios/Pitchy.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@

#ifdef RCT_NEW_ARCH_ENABLED
#import "RNPitchySpec.h"
#import <React/RCTEventEmitter.h>

@interface Pitchy : NSObject <NativePitchySpec>
@interface Pitchy : RCTEventEmitter <NativePitchySpec>
#else
#import <React/RCTBridgeModule.h>
#import <React/RCTEventEmitter.h>

@interface Pitchy : NSObject <RCTBridgeModule>
@interface Pitchy : RCTEventEmitter <RCTBridgeModule>
#endif

@end
95 changes: 69 additions & 26 deletions ios/Pitchy.mm
Original file line number Diff line number Diff line change
@@ -1,41 +1,84 @@
#import "Pitchy.h"
#import <AVFoundation/AVFoundation.h>

@implementation Pitchy {
AVAudioEngine *audioEngine;
double sampleRate;
double minVolume;
BOOL isRecording;
BOOL isInitialized;
}

@implementation Pitchy
RCT_EXPORT_MODULE()

// Method to expose autoCorrelate
RCT_EXPORT_METHOD(autoCorrelate:(NSArray<NSNumber *> *)buf
sampleRate:(double)sampleRate
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)
{
// Convert NSArray<NSNumber *> to std::vector<double>
std::vector<double> cBuf;
for (NSNumber *num in buf) {
cBuf.push_back([num doubleValue]);
- (NSArray<NSString *> *)supportedEvents {
return @[@"onPitchDetected"];
}

RCT_EXPORT_METHOD(init:(NSDictionary *)config) {
if (!isInitialized) {
audioEngine = [[AVAudioEngine alloc] init];
AVAudioInputNode *inputNode = [audioEngine inputNode];

AVAudioFormat *format = [inputNode inputFormatForBus:0];
sampleRate = format.sampleRate;
minVolume = [config[@"minVolume"] doubleValue];

[inputNode installTapOnBus:0 bufferSize:[config[@"bufferSize"] unsignedIntValue] format:format block:^(AVAudioPCMBuffer * _Nonnull buffer, AVAudioTime * _Nonnull when) {
[self detectPitch:buffer];
}];

isInitialized = YES;
}
}

// Call the autoCorrelate function
double result = pitchy::autoCorrelate(cBuf, sampleRate);
RCT_EXPORT_METHOD(isRecording:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject) {
resolve([NSNumber numberWithBool:isRecording]);
}

resolve([NSNumber numberWithDouble:result]);
RCT_EXPORT_METHOD(start:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject) {
if (!isInitialized) {
reject(@"not_initialized", @"Pitchy module is not initialized", nil);
return;
}

if(isRecording){
reject(@"already_recording", @"Already recording", nil);
return;
}

NSError *error = nil;
[audioEngine startAndReturnError:&error];
if (error) {
reject(@"start_error", @"Failed to start audio engine", error);
} else {
isRecording = YES;
resolve(@(YES));
}
}

// Method to expose calculateVolume
RCT_EXPORT_METHOD(calculateVolume:(NSArray<NSNumber *> *)buf
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)
{
// Convert NSArray<NSNumber *> to std::vector<double>
std::vector<double> cBuf;
for (NSNumber *num in buf) {
cBuf.push_back([num doubleValue]);
RCT_EXPORT_METHOD(stop:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject) {

if (!isRecording) {
reject(@"not_recording", @"Not recording", nil);
return;
}

// Call the calculateVolume function
double result = pitchy::calculateVolume(cBuf);
[audioEngine stop];
isRecording = NO;
resolve(@(YES));
}

- (void)detectPitch:(AVAudioPCMBuffer *)buffer {
float *channelData = buffer.floatChannelData[0];
std::vector<double> buf(channelData, channelData + buffer.frameLength);

resolve([NSNumber numberWithDouble:result]);
double detectedPitch = pitchy::autoCorrelate(buf, sampleRate, minVolume);

[self sendEventWithName:@"onPitchDetected" body:@{@"pitch": @(detectedPitch)}];
}

@end
Loading

0 comments on commit 52b24b5

Please sign in to comment.