diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1652bb7..4728cd3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,11 +7,13 @@ on: paths: - '**' - '!README.md' + - '!example/README.md' pull_request: branches: [ main, 'v[0-9]+.[0-9]+' ] paths: - '**' - '!README.md' + - '!example/README.md' jobs: build: @@ -19,15 +21,13 @@ jobs: strategy: matrix: - node-version: [lts/*] + node-version: [16.x, 18.x, 20.x] steps: - - uses: actions/checkout@v2 - with: - submodules: recursive + - uses: actions/checkout@v3 - name: Set up Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} diff --git a/README.md b/README.md index 2c56797..7083995 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,166 @@ -# react-native-voice-processor +# React Native Voice Processor -A native module for reading audio and passing audio buffers to the React Native Javascript layer. +[![GitHub release](https://img.shields.io/github/release/Picovoice/react-native-voice-processor.svg)](https://github.com/Picovoice/react-native-voice-processor/releases) +[![GitHub](https://img.shields.io/github/license/Picovoice/react-native-voice-processor)](https://github.com/Picovoice/react-native-voice-processor/) + +[![npm](https://img.shields.io/npm/v/@picovoice/react-native-voice-processor)](https://www.npmjs.com/package/@picovoice/react-native-voice-processor) + +Made in Vancouver, Canada by [Picovoice](https://picovoice.ai) + + +[![Twitter URL](https://img.shields.io/twitter/url?label=%40AiPicovoice&style=social&url=https%3A%2F%2Ftwitter.com%2FAiPicovoice)](https://twitter.com/AiPicovoice) + +[![YouTube Channel Views](https://img.shields.io/youtube/channel/views/UCAdi9sTCXLosG1XeqDwLx7w?label=YouTube&style=social)](https://www.youtube.com/channel/UCAdi9sTCXLosG1XeqDwLx7w) + +The React Native Voice Processor is an asynchronous audio capture library designed for real-time audio +processing on mobile devices. Given some specifications, the library delivers frames of raw audio +data to the user via listeners. + +## Table of Contents + +- [React Native Voice Processor](#react-native-voice-processor) + - [Table of Contents](#table-of-contents) + - [Requirements](#requirements) + - [Compatibility](#compatibility) + - [Installation](#installation) + - [Permissions](#permissions) + - [Usage](#usage) + - [Capturing with Multiple Listeners](#capturing-with-multiple-listeners) + - [Example](#example) + +## Requirements + +- [Node.js](https://nodejs.org) (16+) +- [Android SDK](https://developer.android.com/about/versions/12/setup-sdk) (21+) +- [JDK](https://www.oracle.com/java/technologies/downloads/) (8+) +- [Xcode](https://developer.apple.com/xcode/) (11+) +- [CocoaPods](https://cocoapods.org/) + +## Compatibility + +- React Native 0.68.7+ +- Android 5.0+ (API 21+) +- iOS 11.0+ + +## Installation + +React Native Voice Processor is available via [npm](https://www.npmjs.com/package/@picovoice/react-native-voice-processor). +To import it into your React Native project install with npm or yarn: +```console +yarn add @picovoice/react-native-voice-processor +``` +or +```console +npm i @picovoice/react-native-voice-processor --save +``` + +## Permissions + +To enable recording with the hardware's microphone, you must first ensure that you have enabled the proper permission on both iOS and Android. + +On iOS, open the `Info.plist` file and add the following line: +```xml +NSMicrophoneUsageDescription +[Permission explanation] +``` + +On Android, open the `AndroidManifest.xml` and add the following line: +```xml + +``` + +See our [example app](./example) for how to properly request this permission from your users. + +## Usage + +Access the singleton instance of `VoiceProcessor`: + +```typescript +import { + VoiceProcessor, + VoiceProcessorError +} from '@picovoice/react-native-voice-processor'; + +let voiceProcessor = VoiceProcessor.instance; +``` + +Add listeners for audio frames and errors: + +```typescript +voiceProcessor.addFrameListener((frame: number[]) => { + // use audio frame +}); +voiceProcessor.addErrorListener((error: VoiceProcessorError) => { + // handle error +}); +``` + +Ask for audio record permission and start recording with the desired frame length and audio sample rate: + +```typescript +const frameLength = 512; +const sampleRate = 16000; + +try { + if (await voiceProcessor.hasRecordAudioPermission()) { + await voiceProcessor.start(frameLength, sampleRate); + } else { + // user did not grant permission + } +} catch (e) { + // handle start error +} +``` + +Stop audio capture: +```typescript +try { + await this._voiceProcessor.stop(); +} catch (e) { + // handle stop error +} +``` + +Once audio capture has started successfully, any frame listeners assigned to the `VoiceProcessor` will start receiving audio frames with the given `frameLength` and `sampleRate`. + +### Capturing with Multiple Listeners + +Any number of listeners can be added to and removed from the `VoiceProcessor` instance. However, +the instance can only record audio with a single audio configuration (`frameLength` and `sampleRate`), +which all listeners will receive once a call to `start()` has been made. To add multiple listeners: +```typescript +const listener1 = (frame) => { }; +const listener2 = (frame) => { }; +const listeners = [listener1, listener2]; +voiceProcessor.addFrameListeners(listeners); + +voiceProcessor.removeFrameListeners(listeners); +// or +voiceProcessor.clearFrameListeners(); +``` + +## Example + +The [React Native Voice Processor app](./example) demonstrates how to ask for user permissions and capture output from the `VoiceProcessor`. + +To launch the demo, run: +```console +yarn bootstrap +yarn example ios +# or +yarn example android +``` + +## Releases + +### v1.2.0 - August 11, 2023 +- Numerous API improvements +- Error handling improvements +- Allow for multiple listeners +- Upgrades to testing infrastructure and example app + +### v1.1.0 - February 23, 2023 +- Migrated to new template + +### v1.0.0 - March 29, 2021 +- Initial public release diff --git a/android/build.gradle b/android/build.gradle index 43b6fbf..5f6a48f 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -63,6 +63,7 @@ dependencies { // For > 0.71, this will be replaced by `com.facebook.react:react-android:$version` by react gradle plugin //noinspection GradleDynamicVersion implementation "com.facebook.react:react-native" + implementation 'ai.picovoice:android-voice-processor:1.0.2' } if (isNewArchitectureEnabled()) { diff --git a/android/src/main/java/ai/picovoice/reactnative/voiceprocessor/VoiceProcessorModule.java b/android/src/main/java/ai/picovoice/reactnative/voiceprocessor/VoiceProcessorModule.java index da86b2d..31fd705 100644 --- a/android/src/main/java/ai/picovoice/reactnative/voiceprocessor/VoiceProcessorModule.java +++ b/android/src/main/java/ai/picovoice/reactnative/voiceprocessor/VoiceProcessorModule.java @@ -11,44 +11,73 @@ package ai.picovoice.reactnative.voiceprocessor; -import androidx.annotation.NonNull; +import android.Manifest; +import android.content.Context; +import android.content.pm.PackageManager; -import android.media.AudioFormat; -import android.media.AudioRecord; -import android.media.MediaRecorder; -import android.os.Process; -import android.util.Log; +import androidx.annotation.NonNull; import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.WritableArray; -import com.facebook.react.bridge.Promise; import com.facebook.react.module.annotations.ReactModule; import com.facebook.react.modules.core.DeviceEventManagerModule; +import com.facebook.react.modules.core.PermissionAwareActivity; +import com.facebook.react.modules.core.PermissionListener; import java.util.HashMap; import java.util.Map; -import java.util.concurrent.Callable; -import java.util.concurrent.Executors; -import java.util.concurrent.atomic.AtomicBoolean; + +import ai.picovoice.android.voiceprocessor.VoiceProcessor; +import ai.picovoice.android.voiceprocessor.VoiceProcessorErrorListener; +import ai.picovoice.android.voiceprocessor.VoiceProcessorException; +import ai.picovoice.android.voiceprocessor.VoiceProcessorFrameListener; @ReactModule(name = VoiceProcessorModule.NAME) public class VoiceProcessorModule extends ReactContextBaseJavaModule { public static final String NAME = "PvVoiceProcessor"; + private static final int RECORD_AUDIO_REQUEST_CODE = + VoiceProcessorModule.class.hashCode(); private static final String LOG_TAG = "PicovoiceVoiceProcessorModule"; - private static final String BUFFER_EMITTER_KEY = "buffer_sent"; - private final ReactApplicationContext context; + private static final String FRAME_EMITTER_KEY = "frame_sent"; + private static final String ERROR_EMITTER_KEY = "error_sent"; - private final AtomicBoolean started = new AtomicBoolean(false); - private final AtomicBoolean stop = new AtomicBoolean(false); - private final AtomicBoolean stopped = new AtomicBoolean(false); + private final ReactApplicationContext context; + private final VoiceProcessor voiceProcessor; - public VoiceProcessorModule(ReactApplicationContext reactContext) { + public VoiceProcessorModule(final ReactApplicationContext reactContext) { super(reactContext); this.context = reactContext; + this.voiceProcessor = VoiceProcessor.getInstance(); + this.voiceProcessor.addErrorListener(new VoiceProcessorErrorListener() { + @Override + public void onError(VoiceProcessorException error) { + reactContext.getJSModule( + DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit( + ERROR_EMITTER_KEY, + error.getMessage()); + } + }); + + this.voiceProcessor.addFrameListener(new VoiceProcessorFrameListener() { + @Override + public void onFrame(final short[] frame) { + WritableArray wArray = Arguments.createArray(); + for (short value : frame) { + wArray.pushInt(value); + } + reactContext.getJSModule( + DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit( + FRAME_EMITTER_KEY, + wArray); + } + }); } @Override @@ -58,110 +87,79 @@ public String getName() { } @ReactMethod - public void addListener(String eventName) { } + public void addListener(String eventName) { + } @ReactMethod - public void removeListeners(Integer count) { } + public void removeListeners(Integer count) { + } @Override public Map getConstants() { final Map constants = new HashMap<>(); - constants.put("BUFFER_EMITTER_KEY", BUFFER_EMITTER_KEY); + constants.put("FRAME_EMITTER_KEY", FRAME_EMITTER_KEY); + constants.put("ERROR_EMITTER_KEY", ERROR_EMITTER_KEY); return constants; } @ReactMethod - public void start(Integer frameSize, Integer sampleRate, Promise promise) { - - if (started.get()) { - return; - } - - Executors.newSingleThreadExecutor().submit(new Callable() { - @Override - public Void call() { - android.os.Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_AUDIO); - read(frameSize, sampleRate); - return null; - } - }); - - while(!started.get()){ - try { - Thread.sleep(10); - } catch (InterruptedException e) { - Log.e(LOG_TAG, e.toString()); - } - } - - promise.resolve(true); + public void start(Integer frameLength, Integer sampleRate, Promise promise) { + try { + voiceProcessor.start(frameLength, sampleRate); + promise.resolve(true); + } catch (VoiceProcessorException e) { + promise.reject( + "PV_AUDIO_RECORDER_ERROR", + "Unable to start audio recording: " + e); + } } @ReactMethod public void stop(Promise promise) { - if (!started.get()) { - return; - } - - stop.set(true); - - while (!stopped.get()) { - try { - Thread.sleep(10); - } catch (InterruptedException e) { - Log.e(LOG_TAG, e.toString()); - } + try { + voiceProcessor.stop(); + promise.resolve(true); + } catch (VoiceProcessorException e) { + promise.reject( + "PV_AUDIO_RECORDER_ERROR", + "Unable to stop audio recording: " + e); } - - started.set(false); - stop.set(false); - stopped.set(false); - promise.resolve(true); } + @ReactMethod + public void isRecording(Promise promise) { + promise.resolve(voiceProcessor.getIsRecording()); + } - private void read(Integer frameSize, Integer sampleRate) { - final int minBufferSize = AudioRecord.getMinBufferSize( - sampleRate, - AudioFormat.CHANNEL_IN_MONO, - AudioFormat.ENCODING_PCM_16BIT); - final int bufferSize = Math.max(sampleRate / 2, minBufferSize); - short[] buffer = new short[frameSize]; - - AudioRecord audioRecord = null; - try { - audioRecord = new AudioRecord( - MediaRecorder.AudioSource.MIC, - sampleRate, - AudioFormat.CHANNEL_IN_MONO, - AudioFormat.ENCODING_PCM_16BIT, - bufferSize); - - audioRecord.startRecording(); - boolean firstBuffer = true; - while (!stop.get()) { - if (audioRecord.read(buffer, 0, buffer.length) == buffer.length) { - if(firstBuffer){ - started.set(true); - firstBuffer = false; + @ReactMethod + public void hasRecordAudioPermission(final Promise promise) { + if (voiceProcessor.hasRecordAudioPermission(this.context.getApplicationContext())) { + promise.resolve(true); + } else { + if (this.getCurrentActivity() != null) { + ((PermissionAwareActivity) this.getCurrentActivity()).requestPermissions( + new String[]{Manifest.permission.RECORD_AUDIO}, + RECORD_AUDIO_REQUEST_CODE, + new PermissionListener() { + public boolean onRequestPermissionsResult(final int requestCode, + @NonNull final String[] permissions, + @NonNull final int[] grantResults) { + if (requestCode == RECORD_AUDIO_REQUEST_CODE && + grantResults.length > 0 && + grantResults[0] == PackageManager.PERMISSION_GRANTED) { + promise.resolve(true); + return true; + } else { + promise.resolve(false); + return false; + } } - WritableArray wArray = Arguments.createArray(); - for (int i = 0; i < buffer.length; i++) - wArray.pushInt(buffer[i]); - this.context.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) - .emit(BUFFER_EMITTER_KEY, wArray); - } + }); + } else { + promise.reject( + "PV_AUDIO_RECORDER_ERROR", + "Unable to access current activity to request permissions"); } - - audioRecord.stop(); - } catch (IllegalArgumentException | IllegalStateException e) { - throw e; - } finally { - if (audioRecord != null) { - audioRecord.release(); - } - - stopped.set(true); } } } diff --git a/example/README.md b/example/README.md new file mode 100644 index 0000000..a7e4ac9 --- /dev/null +++ b/example/README.md @@ -0,0 +1,36 @@ +# React Native Voice Processor Example + +This is an example app that demonstrates how to ask for user permissions and capture output from +the `VoiceProcessor`. + +## Requirements + +- [Node.js](https://nodejs.org) (16+) +- [Android SDK](https://developer.android.com/about/versions/12/setup-sdk) (21+) +- [JDK](https://www.oracle.com/java/technologies/downloads/) (8+) +- [Xcode](https://developer.apple.com/xcode/) (11+) +- [CocoaPods](https://cocoapods.org/) + +## Compatibility + +- Android 5.0+ (API 21+) +- iOS 11.0+ + +## Building + +Install dependencies and setup environment (from the root directory of the repo): + +```console +yarn bootstrap +``` + +Connect a mobile device or launch a simulator. Then build and run the app: +```console +yarn example ios +# or +yarn example android +``` + +## Usage + +Toggle recording on and off with the button in the center of the screen. While recording, the VU meter on the screen will respond to the volume of incoming audio. diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index ef716c1..d5005ec 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -76,6 +76,7 @@ PODS: - hermes-engine (0.71.3): - hermes-engine/Pre-built (= 0.71.3) - hermes-engine/Pre-built (0.71.3) + - ios-voice-processor (1.1.0) - libevent (2.1.12) - OpenSSL-Universal (1.1.1100) - RCT-Folly (2021.07.22.00): @@ -329,7 +330,8 @@ PODS: - React-jsinspector (0.71.3) - React-logger (0.71.3): - glog - - react-native-voice-processor (1.1.0): + - react-native-voice-processor (1.2.0): + - ios-voice-processor (~> 1.1.0) - React-Core - React-perflogger (0.71.3) - React-RCTActionSheet (0.71.3): @@ -415,7 +417,7 @@ PODS: - React-jsi (= 0.71.3) - React-logger (= 0.71.3) - React-perflogger (= 0.71.3) - - SocketRocket (0.6.0) + - SocketRocket (0.6.1) - Yoga (1.14.0) - YogaKit (1.18.1): - Yoga (~> 1.14) @@ -495,6 +497,7 @@ SPEC REPOS: - Flipper-RSocket - FlipperKit - fmt + - ios-voice-processor - libevent - OpenSSL-Universal - SocketRocket @@ -590,6 +593,7 @@ SPEC CHECKSUMS: fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9 glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b hermes-engine: 38bfe887e456b33b697187570a08de33969f5db7 + ios-voice-processor: 8e32d7f980a06d392d128ef1cd19cf6ddcaca3c1 libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 OpenSSL-Universal: ebc357f1e6bc71fa463ccb2fe676756aff50e88c RCT-Folly: 424b8c9a7a0b9ab2886ffe9c3b041ef628fd4fb1 @@ -606,7 +610,7 @@ SPEC CHECKSUMS: React-jsiexecutor: 515b703d23ffadeac7687bc2d12fb08b90f0aaa1 React-jsinspector: 9f7c9137605e72ca0343db4cea88006cb94856dd React-logger: 957e5dc96d9dbffc6e0f15e0ee4d2b42829ff207 - react-native-voice-processor: 3d396bfd42d11ee270bf292ce7cc935ea62d4cae + react-native-voice-processor: 7504bbbf53974ef57a184b0cf814873cf5e4a2a6 React-perflogger: af8a3d31546077f42d729b949925cc4549f14def React-RCTActionSheet: 57cc5adfefbaaf0aae2cf7e10bccd746f2903673 React-RCTAnimation: 11c61e94da700c4dc915cf134513764d87fc5e2b @@ -620,7 +624,7 @@ SPEC CHECKSUMS: React-RCTVibration: 5199a180d04873366a83855de55ac33ce60fe4d5 React-runtimeexecutor: 7bf0dafc7b727d93c8cb94eb00a9d3753c446c3e ReactCommon: 6f65ea5b7d84deb9e386f670dd11ce499ded7b40 - SocketRocket: fccef3f9c5cedea1353a9ef6ada904fde10d6608 + SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 Yoga: 5ed1699acbba8863755998a4245daa200ff3817b YogaKit: f782866e155069a2cca2517aafea43200b01fd5a diff --git a/example/ios/VoiceProcessorExample.xcodeproj/project.pbxproj b/example/ios/VoiceProcessorExample.xcodeproj/project.pbxproj index 3260f5f..5b2f5ea 100644 --- a/example/ios/VoiceProcessorExample.xcodeproj/project.pbxproj +++ b/example/ios/VoiceProcessorExample.xcodeproj/project.pbxproj @@ -378,7 +378,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.4; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = ( /usr/lib/swift, "$(inherited)", @@ -443,7 +443,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.4; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = ( /usr/lib/swift, "$(inherited)", diff --git a/example/package.json b/example/package.json index 299de58..d14aa71 100644 --- a/example/package.json +++ b/example/package.json @@ -10,13 +10,14 @@ }, "dependencies": { "react": "18.2.0", - "react-native": "0.71.3" + "react-native": "0.71.12", + "react-native-svg": "^13.11.0" }, "devDependencies": { "@babel/core": "^7.20.0", "@babel/preset-env": "^7.20.0", "@babel/runtime": "^7.20.0", - "metro-react-native-babel-preset": "0.73.7", - "babel-plugin-module-resolver": "^4.1.0" + "babel-plugin-module-resolver": "^4.1.0", + "metro-react-native-babel-preset": "0.73.7" } -} \ No newline at end of file +} diff --git a/example/src/App.tsx b/example/src/App.tsx index 9835296..5caa552 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -10,25 +10,32 @@ // import React, { Component } from 'react'; -import { Button, PermissionsAndroid, Platform } from 'react-native'; +import { Button, StyleSheet, Text, View } from 'react-native'; +import Svg, { Rect } from 'react-native-svg'; + import { - StyleSheet, - View, - EventSubscription, - NativeEventEmitter, -} from 'react-native'; -import { VoiceProcessor, BufferEmitter } from 'react-native-voice-processor'; + VoiceProcessor, + VoiceProcessorError, + // @ts-ignore +} from '@picovoice/react-native-voice-processor'; type Props = {}; type State = { isListening: boolean; buttonText: string; buttonDisabled: boolean; + errorMessage: string | null; + vuMeterWidthPercent: number; + volumeHistory: number[]; }; export default class App extends Component { - _bufferListener?: EventSubscription; - _bufferEmitter: NativeEventEmitter; + private readonly _frameLength: number = 512; + private readonly _sampleRate: number = 16000; + private readonly _dbfsOffset: number = 60; + private readonly _volumeHistoryCapacity: number = 5; + + private _voiceProcessor: VoiceProcessor; constructor(props: Props) { super(props); @@ -36,104 +43,133 @@ export default class App extends Component { buttonText: 'Start', isListening: false, buttonDisabled: false, + errorMessage: null, + vuMeterWidthPercent: 0, + volumeHistory: new Array(this._volumeHistoryCapacity).fill(0), }; - this._bufferEmitter = new NativeEventEmitter(BufferEmitter); - this._bufferListener = this._bufferEmitter.addListener( - BufferEmitter.BUFFER_EMITTER_KEY, - async (buffer: number[]) => { - console.log(`Buffer of size ${buffer.length} received!`); - } - ); + this._voiceProcessor = VoiceProcessor.instance; + this._voiceProcessor.addFrameListener((frame: number[]) => { + this.setState((prevState) => ({ + volumeHistory: [ + ...prevState.volumeHistory.splice(1), + this.calculateVolume(frame), + ], + })); + }); + this._voiceProcessor.addErrorListener((error: VoiceProcessorError) => { + this.setState({ + errorMessage: `Error received from error listener: ${error}`, + }); + }); } - componentDidMount() {} - - _startProcessing() { - let recordAudioRequest; - if (Platform.OS === 'android') { - recordAudioRequest = this._requestRecordAudioPermission(); - } else { - recordAudioRequest = new Promise(function (resolve, _) { - resolve(true); + componentDidUpdate(_prevProps: Props, prevState: State) { + if (prevState.volumeHistory !== this.state.volumeHistory) { + const volumeAvg = + [...this.state.volumeHistory].reduce( + (accumulator, value) => accumulator + value, + 0 + ) / this._volumeHistoryCapacity; + this.setState({ + vuMeterWidthPercent: volumeAvg * 100, }); } + } - recordAudioRequest.then((hasPermission) => { - if (!hasPermission) { - console.error('Did not grant required microphone permission.'); - return; - } - - VoiceProcessor.getVoiceProcessor(512, 16000) - .start() - .then((didStart) => { - if (didStart) { - this.setState({ - isListening: true, - buttonText: 'Stop', - buttonDisabled: false, - }); - } + async _startProcessing() { + try { + if (await this._voiceProcessor.hasRecordAudioPermission()) { + await this._voiceProcessor.start(this._frameLength, this._sampleRate); + this.setState({ + isListening: await this._voiceProcessor.isRecording(), + buttonText: 'Stop', + buttonDisabled: false, }); - }); + } else { + this.setState({ + errorMessage: 'User did not grant permission to record audio', + }); + } + } catch (e) { + this.setState({ + errorMessage: `Unable to start recording: ${e}`, + }); + } } - _stopProcessing() { - VoiceProcessor.getVoiceProcessor(512, 16000) - .stop() - .then((didStop) => { - if (didStop) { - this.setState({ - isListening: false, - buttonText: 'Start', - buttonDisabled: false, - }); - } + async _stopProcessing() { + try { + await this._voiceProcessor.stop(); + this.setState({ + isListening: await this._voiceProcessor.isRecording(), + buttonText: 'Start', + buttonDisabled: false, + }); + } catch (e) { + this.setState({ + errorMessage: `Unable to stop recording: ${e}`, }); + } } - _toggleProcessing() { + async _toggleProcessing() { this.setState({ buttonDisabled: true, }); if (this.state.isListening) { - this._stopProcessing(); + await this._stopProcessing(); } else { - this._startProcessing(); + await this._startProcessing(); } } - async _requestRecordAudioPermission() { - try { - const granted = await PermissionsAndroid.request( - PermissionsAndroid.PERMISSIONS.RECORD_AUDIO!, - { - title: 'Microphone Permission', - message: - 'VoiceProcessorExample needs your permission to receive audio buffers.', - buttonNeutral: 'Ask Me Later', - buttonNegative: 'Cancel', - buttonPositive: 'OK', - } - ); - return granted === PermissionsAndroid.RESULTS.GRANTED; - } catch (err) { - console.error(err); - return false; - } + calculateVolume(frame: number[]): number { + const sum = [...frame].reduce( + (accumulator, sample) => accumulator + sample ** 2, + 0 + ); + const rms = Math.sqrt(sum / frame.length) / 32767.0; + const dbfs = 20 * Math.log10(Math.max(rms, 1e-9)); + return Math.min(1, Math.max(0, dbfs + this._dbfsOffset) / this._dbfsOffset); } render() { return ( - -