Skip to content

Commit b4495ca

Browse files
authored
recorder-metering (react-native-audio-toolkit#221)
1 parent 6a258f1 commit b4495ca

File tree

4 files changed

+157
-5
lines changed

4 files changed

+157
-5
lines changed

android/src/main/java/com/reactnativecommunity/rctaudiotoolkit/AudioRecorderModule.java

Lines changed: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,13 @@
22

33
import android.annotation.TargetApi;
44
import android.media.MediaRecorder;
5-
import android.os.Build;
6-
import android.os.Environment;
75
import android.util.Log;
86
import android.net.Uri;
97
import android.webkit.URLUtil;
108
import android.content.ContextWrapper;
119

1210
import com.facebook.react.bridge.Arguments;
1311
import com.facebook.react.bridge.ReactApplicationContext;
14-
import com.facebook.react.bridge.ReactContext;
1512
import com.facebook.react.bridge.ReactContextBaseJavaModule;
1613
import com.facebook.react.bridge.ReactMethod;
1714
import com.facebook.react.bridge.Callback;
@@ -23,11 +20,12 @@
2320
import java.io.IOException;
2421
import java.io.File;
2522
import java.lang.Thread;
26-
import java.net.URISyntaxException;
2723
import java.util.HashMap;
2824
import java.util.Map;
2925
import java.util.Map.Entry;
3026
import java.util.Objects;
27+
import java.util.Timer;
28+
import java.util.TimerTask;
3129

3230
public class AudioRecorderModule extends ReactContextBaseJavaModule implements
3331
MediaRecorder.OnInfoListener, MediaRecorder.OnErrorListener {
@@ -37,6 +35,11 @@ public class AudioRecorderModule extends ReactContextBaseJavaModule implements
3735
Map<Integer, Boolean> recorderAutoDestroy = new HashMap<>();
3836

3937
private ReactApplicationContext context;
38+
private Timer meteringUpdateTimer;
39+
private int meteringFrameId = 0;
40+
private Integer meteringRecorderId = null;
41+
private MediaRecorder meteringRecorder = null;
42+
private int meteringInterval = 0;
4043

4144
public AudioRecorderModule(ReactApplicationContext reactContext) {
4245
super(reactContext);
@@ -140,6 +143,39 @@ private Uri uriFromPath(String path) {
140143

141144
return uri;
142145
}
146+
147+
// metering methods
148+
private void startMeteringTimer(int monitorInterval) {
149+
meteringUpdateTimer = new Timer();
150+
meteringUpdateTimer.scheduleAtFixedRate(new TimerTask() {
151+
@Override
152+
public void run() {
153+
if(meteringRecorderId != null && meteringRecorder != null) {
154+
WritableMap body = Arguments.createMap();
155+
body.putDouble("id", meteringFrameId++);
156+
157+
int amplitude = meteringRecorder.getMaxAmplitude();
158+
if (amplitude == 0) {
159+
body.putInt("value", -160);
160+
body.putInt("rawValue", 0);
161+
} else {
162+
body.putInt("rawValue", amplitude);
163+
body.putInt("value", (int) (20 * Math.log10(((double) amplitude) / 32767d)));
164+
}
165+
emitEvent(meteringRecorderId, "meter", body);
166+
}
167+
}
168+
}, 0, monitorInterval);
169+
}
170+
171+
private void stopMeteringTimer() {
172+
if (meteringUpdateTimer != null) {
173+
meteringUpdateTimer.cancel();
174+
meteringUpdateTimer.purge();
175+
meteringUpdateTimer = null;
176+
meteringFrameId = 0;
177+
}
178+
}
143179

144180
@ReactMethod
145181
public void destroy(Integer recorderId, Callback callback) {
@@ -149,6 +185,10 @@ public void destroy(Integer recorderId, Callback callback) {
149185
recorder.release();
150186
this.recorderPool.remove(recorderId);
151187
this.recorderAutoDestroy.remove(recorderId);
188+
if(recorderId == meteringRecorderId) {
189+
meteringRecorderId = null;
190+
meteringRecorder = null;
191+
}
152192

153193
WritableMap data = new WritableNativeMap();
154194
data.putString("message", "Destroyed recorder");
@@ -240,6 +280,20 @@ public void prepare(Integer recorderId, String path, ReadableMap options, Callba
240280
} catch (IOException e) {
241281
callback.invoke(errObj("preparefail", e.toString()));
242282
}
283+
284+
if (options.hasKey("meteringInterval")) {
285+
int meteringInterval = options.getInt("meteringInterval");
286+
if (meteringRecorderId != null) {
287+
Log.i(LOG_TAG, "multiple recorder metering are not currently supporter. Metering will be active on the last recorder.");
288+
}
289+
if(meteringInterval <= 0) {
290+
Log.w(LOG_TAG, "metering interval must be grater then 0. Ignoring metering");
291+
} else {
292+
meteringRecorder = recorder;
293+
meteringRecorderId = recorderId;
294+
this.meteringInterval = meteringInterval;
295+
}
296+
}
243297
}
244298

245299
@ReactMethod
@@ -251,6 +305,9 @@ public void record(Integer recorderId, Callback callback) {
251305
}
252306

253307
try {
308+
if(recorderId == meteringRecorderId) {
309+
startMeteringTimer(meteringInterval);
310+
}
254311
recorder.start();
255312

256313
callback.invoke();
@@ -268,6 +325,9 @@ public void stop(Integer recorderId, Callback callback) {
268325
}
269326

270327
try {
328+
if(recorderId == meteringRecorderId) {
329+
stopMeteringTimer();
330+
}
271331
recorder.stop();
272332
if (this.recorderAutoDestroy.get(recorderId)) {
273333
Log.d(LOG_TAG, "Autodestroying recorder...");
@@ -298,6 +358,9 @@ private void pause24(Integer recorderId, Callback callback) {
298358
}
299359

300360
try {
361+
if(recorderId == meteringRecorderId) {
362+
stopMeteringTimer();
363+
}
301364
recorder.pause();
302365
if (this.recorderAutoDestroy.get(recorderId)) {
303366
Log.d(LOG_TAG, "Autodestroying recorder...");

docs/API.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,11 @@ Player.isPrepared true if player is prepared
220220
// Quality of the recording, iOS only.
221221
// Possible values: 'min', 'low', 'medium', 'high', 'max'
222222
quality : String (default: 'medium')
223+
224+
// Optional argument to activate metering events
225+
// this will cause 'meter' event to fire everi given milliseconds.
226+
// i.e. 250 will fire 4 time in a second.
227+
meteringInterval : Number (default: undefined)
223228
}
224229
```
225230

@@ -310,6 +315,15 @@ are supported:
310315
311316
* `looped` - Playback of a file has looped.
312317
318+
* `meter` - recurring event during recording session (see `meteringInterval` in `recorderOptions`). `data` associated to this event follows the format:
319+
```js
320+
{
321+
"id", // frame number
322+
"value", // sound level in decibels, -160 is a silence level
323+
"rawValue" // raw level value, OS-dependent
324+
}
325+
```
326+
**Currently, only one recored at a time generates meter events. Last prepared Recorder wins.**
313327
314328
Listen to these events with `player.on('eventname', callback(data))`. Data
315329
may contain additional information about the event, for example a more detailed

ios/ReactNativeAudioToolkit/ReactNativeAudioToolkit/AudioRecorder.m

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,19 @@ @interface AudioRecorder () <AVAudioRecorderDelegate>
2121

2222
@end
2323

24-
@implementation AudioRecorder
24+
@implementation AudioRecorder {
25+
id _meteringUpdateTimer;
26+
int _meteringFrameId;
27+
int _meteringUpdateInterval;
28+
NSNumber *_meteringRecorderId;
29+
AVAudioRecorder *_meteringRecorder;
30+
NSDate *_prevMeteringUpdateTime;
31+
}
2532

2633
@synthesize bridge = _bridge;
2734

2835
- (void)dealloc {
36+
[self stopMeteringTimer];
2937
AVAudioSession *audioSession = [AVAudioSession sharedInstance];
3038
NSError *error = nil;
3139
[audioSession setActive:NO error:&error];
@@ -47,6 +55,53 @@ -(NSNumber *) keyForRecorder:(nonnull AVAudioRecorder*)recorder {
4755
return [[_recorderPool allKeysForObject:recorder] firstObject];
4856
}
4957

58+
#pragma mark - Metering functions
59+
60+
- (void)stopMeteringTimer {
61+
[_meteringUpdateTimer invalidate];
62+
_meteringFrameId = 0;
63+
_prevMeteringUpdateTime = nil;
64+
_meteringRecorderId = nil;
65+
_meteringRecorder = nil;
66+
}
67+
68+
- (void)startMeteringTimer:(int)monitorInterval {
69+
_meteringUpdateInterval = monitorInterval;
70+
71+
[self stopMeteringTimer];
72+
73+
_meteringUpdateTimer = [CADisplayLink displayLinkWithTarget:self selector:@selector(sendMeteringUpdate)];
74+
[_meteringUpdateTimer addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
75+
}
76+
77+
- (void)sendMeteringUpdate {
78+
if(!_meteringRecorder) {
79+
[self stopMeteringTimer];
80+
return;
81+
}
82+
if (!_meteringRecorder.isRecording) {
83+
return;
84+
}
85+
86+
if (_prevMeteringUpdateTime == nil ||
87+
(([_prevMeteringUpdateTime timeIntervalSinceNow] * -1000.0) >= _meteringUpdateInterval)) {
88+
_meteringFrameId++;
89+
NSMutableDictionary *body = [[NSMutableDictionary alloc] init];
90+
[body setObject:[NSNumber numberWithFloat:_meteringFrameId] forKey:@"id"];
91+
92+
[_meteringRecorder updateMeters];
93+
float _currentLevel = [_meteringRecorder averagePowerForChannel: 0];
94+
[body setObject:[NSNumber numberWithFloat:_currentLevel] forKey:@"value"];
95+
[body setObject:[NSNumber numberWithFloat:_currentLevel] forKey:@"rawValue"];
96+
NSString *eventName = [NSString stringWithFormat:@"RCTAudioRecorderEvent:%@", _meteringRecorderId];
97+
[self.bridge.eventDispatcher sendAppEventWithName:eventName
98+
body:@{@"event" : @"meter",
99+
@"data" : body
100+
}];
101+
_prevMeteringUpdateTime = [NSDate date];
102+
}
103+
}
104+
50105
#pragma mark - React exposed functions
51106

52107
RCT_EXPORT_MODULE();
@@ -127,6 +182,16 @@ -(NSNumber *) keyForRecorder:(nonnull AVAudioRecorder*)recorder {
127182
return;
128183
}
129184

185+
NSNumber *meteringInterval = [options objectForKey:@"meteringInterval"];
186+
if (meteringInterval) {
187+
recorder.meteringEnabled = YES;
188+
[self startMeteringTimer:[meteringInterval intValue]];
189+
if (_meteringRecorderId != nil)
190+
NSLog(@"multiple recorder metering are not currently supporter. Metering will be active on the last recorder.");
191+
_meteringRecorderId = recorderId;
192+
_meteringRecorder = recorder;
193+
}
194+
130195
callback(@[[NSNull null], filePath]);
131196
}
132197

@@ -155,6 +220,9 @@ -(NSNumber *) keyForRecorder:(nonnull AVAudioRecorder*)recorder {
155220
callback(@[dict]);
156221
return;
157222
}
223+
if(recorderId == _meteringRecorderId) {
224+
[self stopMeteringTimer];
225+
}
158226
callback(@[[NSNull null]]);
159227
}
160228

typings/index.d.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,13 @@ interface RecorderOptions {
254254
* (Default: 'medium')
255255
*/
256256
quality: string;
257+
258+
/**
259+
* Set monitor interval in millisecond.
260+
* Passing a value recorder will receive
261+
* db level while recording.
262+
*/
263+
meteringInterval: number;
257264
}
258265

259266
/**

0 commit comments

Comments
 (0)