Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 26 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@

# react-native-live-audio-stream
[![npm](https://img.shields.io/npm/v/react-native-live-audio-stream)](https://www.npmjs.com/package/react-native-live-audio-stream)

Get live audio stream data for React Native. Ideal for live voice recognition (transcribing).
# theInfiTualEr/react-native-live-audio-stream

This module is modified from [react-native-audio-record](https://github.com/goodatlas/react-native-audio-record). Instead of saving to an audio file, it only emit events with live data. By doing this, it can reduce memory usage and eliminate file operation overheads in the case that an audio file is not necessary (e.g. live transcribing).
This package is a modified version of [react-native-live-audio-stream](https://github.com/xiqi/react-native-live-audio-stream) which that itself is a modification of [react-native-audio-record](https://github.com/goodatlas/react-native-audio-record) package. This package adds **play**, **unload** and **generating audio header** functionality to the `react-native-live-audio-stream` package, but **ONLY FOR ANDROID.**

Most of the code was written by the respective original authors.
[![npm](https://img.shields.io/npm/v/react-native-live-audio-stream)](https://www.npmjs.com/package/react-native-live-audio-stream)

## Install
```
yarn add react-native-live-audio-stream
npm install theInfiTualEr/react-native-live-audio-stream
cd ios
pod install
```
Expand All @@ -33,23 +30,44 @@ Add the following line to ```android/app/src/main/AndroidManifest.xml```
## Usage
```javascript
import LiveAudioStream from 'react-native-live-audio-stream';
import { PermissionsAndroid } from "react-native";


const options = {
sampleRate: 32000, // default is 44100 but 32000 is adequate for accurate voice recognition
channels: 1, // 1 or 2, default 1
bitsPerSample: 16, // 8 or 16, default 16
audioSource: 6, // android only (see below)
bufferSize: 4096 // default is 2048
hasAudioHeader: true // default is false, but you probably need it
};

await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.RECORD_AUDIO
);

LiveAudioStream.init(options);
LiveAudioStream.on('data', data => {
// base64-encoded audio data chunks

// below line plays the received audio data
// NOTE: this DOES NOT WORK on iOS
LiveAudioStream.addPlay(data);
});
...
// NOTE: `loadPlayer` is not necessary on iOS
LiveAudioStream.loadPlayer();
// NOTE: `startPlay` is not necessary on iOS
LiveAudioStream.startPlay();
LiveAudioStream.loadRecorder();
LiveAudioStream.start();
...
LiveAudioStream.stop();
LiveAudioStream.unloadRecorder();
// NOTE: `stopPlay` is not necessary on iOS
LiveAudioStream.stopPlay();
// NOTE: `unloadPlayer` is not necessary on iOS
LiveAudioStream.unloadPlayer();
...
```

Expand All @@ -66,6 +84,7 @@ LiveAudioStream.on('data', data => {
```

## Credits/References
- [react-native-live-audio-stream](https://github.com/xiqi/react-native-live-audio-stream)
- [react-native-audio-record](https://github.com/goodatlas/react-native-audio-record)
- iOS [Audio Queues](https://developer.apple.com/library/content/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide)
- Android [AudioRecord](https://developer.android.com/reference/android/media/AudioRecord.html)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.AudioTrack;
import android.media.AudioManager;
import android.media.MediaRecorder.AudioSource;
import android.util.Base64;
import android.util.Log;

import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
Expand All @@ -24,11 +25,15 @@ public class RNLiveAudioStreamModule extends ReactContextBaseJavaModule {
private int channelConfig;
private int audioFormat;
private int audioSource;
private boolean hasAudioHeader;

private AudioTrack player;
private AudioRecord recorder;
private int bufferSize;
private boolean isRecording;

private byte[] wavHeaders;

public RNLiveAudioStreamModule(ReactApplicationContext reactContext) {
super(reactContext);
this.reactContext = reactContext;
Expand All @@ -47,35 +52,38 @@ public void init(ReadableMap options) {
}

channelConfig = AudioFormat.CHANNEL_IN_MONO;
if (options.hasKey("channels")) {
if (options.getInt("channels") == 2) {
channelConfig = AudioFormat.CHANNEL_IN_STEREO;
}
if (options.hasKey("channels") && options.getInt("channels") == 2) {
channelConfig = AudioFormat.CHANNEL_IN_STEREO;
}

audioFormat = AudioFormat.ENCODING_PCM_16BIT;
if (options.hasKey("bitsPerSample")) {
if (options.getInt("bitsPerSample") == 8) {
audioFormat = AudioFormat.ENCODING_PCM_8BIT;
}
if (options.hasKey("bitsPerSample") && options.getInt("bitsPerSample") == 8) {
audioFormat = AudioFormat.ENCODING_PCM_8BIT;
}

audioSource = AudioSource.VOICE_RECOGNITION;
if (options.hasKey("audioSource")) {
audioSource = options.getInt("audioSource");
}

hasAudioHeader = false;
if (options.hasKey("hasAudioHeader")) {
hasAudioHeader = options.getBoolean("hasAudioHeader");
}

isRecording = false;
eventEmitter = reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class);

bufferSize = AudioRecord.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat);

if (options.hasKey("bufferSize")) {
bufferSize = Math.max(bufferSize, options.getInt("bufferSize"));
}

int recordingBufferSize = bufferSize * 3;
recorder = new AudioRecord(audioSource, sampleRateInHz, channelConfig, audioFormat, recordingBufferSize);
// TODO recordingBufferSize or bufferSize ?
// int recordingBufferSize = bufferSize * 3;
long totalAudioLen = bufferSize;
long totalDataLen = totalAudioLen + 36;
wavHeaders = genWavHeader(totalAudioLen, totalDataLen);
}

@ReactMethod
Expand All @@ -96,7 +104,18 @@ public void run() {

// skip first 2 buffers to eliminate "click sound"
if (bytesRead > 0 && ++count > 2) {
base64Data = Base64.encodeToString(buffer, Base64.NO_WRAP);
if (!hasAudioHeader) {
base64Data = Base64.encodeToString(buffer, Base64.NO_WRAP);
eventEmitter.emit("data", base64Data);
continue;
}

byte[] combined = new byte[wavHeaders.length + buffer.length];
System.arraycopy(wavHeaders, 0, combined, 0, wavHeaders.length);
System.arraycopy(buffer, 0, combined, wavHeaders.length, buffer.length);

base64Data = Base64.encodeToString(combined, Base64.NO_WRAP);

eventEmitter.emit("data", base64Data);
}
}
Expand All @@ -114,4 +133,116 @@ public void run() {
public void stop(Promise promise) {
isRecording = false;
}

@ReactMethod
public void startPlay() {
player.play();
}

@ReactMethod
public void addPlay(String audioBufferBase64) {
// player.flush();
byte[] audioBuffer = Base64.decode(audioBufferBase64, Base64.NO_WRAP);
player.write(audioBuffer, 0, audioBuffer.length);
}

@ReactMethod
public void stopPlay() {
player.stop();
}

@ReactMethod
public void loadPlayer() {
// more info:
// https://stackoverflow.com/questions/9413998/live-audio-recording-and-playing-in-android-and-thread-callback-handling
// TODO recordingBufferSize or bufferSize ?
// int recordingBufferSize = bufferSize * 3;
player = new AudioTrack(AudioManager.STREAM_MUSIC,
sampleRateInHz, channelConfig, audioFormat,
bufferSize, AudioTrack.MODE_STREAM);
player.setPlaybackRate(sampleRateInHz);
}

@ReactMethod
public void unloadPlayer() {
player.release();
}

@ReactMethod
public void loadRecorder() {
// int recordingBufferSize = bufferSize * 3;
recorder = new AudioRecord(audioSource, sampleRateInHz, channelConfig, audioFormat, bufferSize);
}

@ReactMethod
public void unloadRecorder() {
recorder.release();
}

@ReactMethod
public void addListener(String eventName) {
// Keep: Required for RN built in Event Emitter Calls.
}

@ReactMethod
public void removeListeners(Integer count) {
// Keep: Required for RN built in Event Emitter Calls.
}

private byte[] genWavHeader(long totalAudioLen, long totalDataLen) {
long sampleRate = sampleRateInHz;
int channels = channelConfig == AudioFormat.CHANNEL_IN_MONO ? 1 : 2;
int bitsPerSample = audioFormat == AudioFormat.ENCODING_PCM_8BIT ? 8 : 16;
long byteRate = sampleRate * channels * bitsPerSample / 8;
int blockAlign = channels * bitsPerSample / 8;

byte[] header = new byte[44];

header[0] = 'R'; // RIFF chunk
header[1] = 'I';
header[2] = 'F';
header[3] = 'F';
header[4] = (byte) (totalDataLen & 0xff); // how big is the rest of this file
header[5] = (byte) ((totalDataLen >> 8) & 0xff);
header[6] = (byte) ((totalDataLen >> 16) & 0xff);
header[7] = (byte) ((totalDataLen >> 24) & 0xff);
header[8] = 'W'; // WAVE chunk
header[9] = 'A';
header[10] = 'V';
header[11] = 'E';
header[12] = 'f'; // 'fmt ' chunk
header[13] = 'm';
header[14] = 't';
header[15] = ' ';
header[16] = 16; // 4 bytes: size of 'fmt ' chunk
header[17] = 0;
header[18] = 0;
header[19] = 0;
header[20] = 1; // format = 1 for PCM
header[21] = 0;
header[22] = (byte) channels; // mono or stereo
header[23] = 0;
header[24] = (byte) (sampleRate & 0xff); // samples per second
header[25] = (byte) ((sampleRate >> 8) & 0xff);
header[26] = (byte) ((sampleRate >> 16) & 0xff);
header[27] = (byte) ((sampleRate >> 24) & 0xff);
header[28] = (byte) (byteRate & 0xff); // bytes per second
header[29] = (byte) ((byteRate >> 8) & 0xff);
header[30] = (byte) ((byteRate >> 16) & 0xff);
header[31] = (byte) ((byteRate >> 24) & 0xff);
header[32] = (byte) blockAlign; // bytes in one sample, for all channels
header[33] = 0;
header[34] = (byte) bitsPerSample; // bits in a sample
header[35] = 0;
header[36] = 'd'; // beginning of the data chunk
header[37] = 'a';
header[38] = 't';
header[39] = 'a';
header[40] = (byte) (totalAudioLen & 0xff); // how big is this data chunk
header[41] = (byte) ((totalAudioLen >> 8) & 0xff);
header[42] = (byte) ((totalAudioLen >> 16) & 0xff);
header[43] = (byte) ((totalAudioLen >> 24) & 0xff);

return header;
}
}
53 changes: 51 additions & 2 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,54 @@
declare module "react-native-live-audio-stream" {
export interface IAudioRecord {
init: (options: Options) => void
/**
* make sure to call `init` before this,
* starts recording.
*/
start: () => void
stop: () => Promise<string>
/**
* stops recording.
*/
stop: () => void
/**
* has to be called before playing audio.
* NOTE: this DOES NOT WORK on iOS
*/
loadPlayer: () => void
/**
* unloads the player resources, can be called on unmount.
* NOTE: this DOES NOT WORK on iOS
*/
unloadPlayer: () => void
/**
* has to be called before playing recorder.
* NOTE: this DOES NOT WORK on iOS
*/
loadRecorder: () => void
/**
* unloads the recorder resources, can be called on unmount.
* NOTE: this DOES NOT WORK on iOS
*/
unloadRecorder: () => void
/**
* make sure to call `init` before this
* NOTE: this DOES NOT WORK on iOS
*/
startPlay: () => void;
/**
* NOTE: this DOES NOT WORK on iOS
* @param audioBufferBase64 same data that you got on `data` event
*/
addPlay: (audioBufferBase64: string) => void;
/**
* NOTE: this DOES NOT WORK on iOS
*/
stopPlay: () => void;
/**
*
* @param event
* @param callback provides data as base64 header-less wave audio
*/
on: (event: "data", callback: (data: string) => void) => void
}

Expand All @@ -20,8 +66,11 @@ declare module "react-native-live-audio-stream" {
* - `6`
*/
audioSource?: number
wavFile: string
bufferSize?: number
/**
* you probably want this to be true if you want to play it elsewhere.
*/
hasAudioHeader?: boolean
}

const AudioRecord: IAudioRecord
Expand Down
31 changes: 31 additions & 0 deletions index.ios.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { NativeModules, NativeEventEmitter } from 'react-native';
const { RNLiveAudioStream } = NativeModules;
const EventEmitter = new NativeEventEmitter(RNLiveAudioStream);

const AudioRecord = {};

AudioRecord.init = options => RNLiveAudioStream.init(options);
AudioRecord.loadPlayer = () => {};
AudioRecord.unloadPlayer = () => {};
AudioRecord.loadRecorder = () => {};
AudioRecord.unloadRecorder = () => {};
AudioRecord.start = () => RNLiveAudioStream.start();
AudioRecord.stop = () => RNLiveAudioStream.stop();
AudioRecord.startPlay = () => {};
AudioRecord.addPlay = () => {};
AudioRecord.stopPlay = () => {};

const eventsMap = {
data: 'data'
};

AudioRecord.on = (event, callback) => {
const nativeEvent = eventsMap[event];
if (!nativeEvent) {
throw new Error('Invalid event');
}
EventEmitter.removeAllListeners(nativeEvent);
return EventEmitter.addListener(nativeEvent, callback);
};

export default AudioRecord;
Loading