Skip to content

Commit 25372df

Browse files
authored
Expose audio sample buffers for Android (#89)
* Initial draft * Working impl * doc and cleanup * doc update
1 parent 5fdfb76 commit 25372df

File tree

8 files changed

+218
-4
lines changed

8 files changed

+218
-4
lines changed

sdk/android/BUILD.gn

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,7 @@ if (is_android) {
255255
"api/org/webrtc/AudioProcessingFactory.java",
256256
"api/org/webrtc/AudioSource.java",
257257
"api/org/webrtc/AudioTrack.java",
258+
"api/org/webrtc/AudioTrackSink.java",
258259
"api/org/webrtc/CallSessionFileRotatingLogSink.java",
259260
"api/org/webrtc/CandidatePairChangeEvent.java",
260261
"api/org/webrtc/CryptoOptions.java",
@@ -716,6 +717,8 @@ if (current_os == "linux" || is_android) {
716717
"src/jni/pc/add_ice_candidate_observer.cc",
717718
"src/jni/pc/add_ice_candidate_observer.h",
718719
"src/jni/pc/android_network_monitor.h",
720+
"src/jni/pc/audio_sink.cc",
721+
"src/jni/pc/audio_sink.h",
719722
"src/jni/pc/audio_track.cc",
720723
"src/jni/pc/call_session_file_rotating_log_sink.cc",
721724
"src/jni/pc/crypto_options.cc",
@@ -1405,6 +1408,7 @@ if (current_os == "linux" || is_android) {
14051408
sources = [
14061409
"api/org/webrtc/AddIceObserver.java",
14071410
"api/org/webrtc/AudioTrack.java",
1411+
"api/org/webrtc/AudioTrackSink.java",
14081412
"api/org/webrtc/CallSessionFileRotatingLogSink.java",
14091413
"api/org/webrtc/CandidatePairChangeEvent.java",
14101414
"api/org/webrtc/CryptoOptions.java",

sdk/android/api/org/webrtc/AudioTrack.java

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,12 @@
1010

1111
package org.webrtc;
1212

13+
import java.util.IdentityHashMap;
14+
1315
/** Java wrapper for a C++ AudioTrackInterface */
1416
public class AudioTrack extends MediaStreamTrack {
17+
private final IdentityHashMap<AudioTrackSink, Long> sinks = new IdentityHashMap<AudioTrackSink, Long>();
18+
1519
public AudioTrack(long nativeTrack) {
1620
super(nativeTrack);
1721
}
@@ -23,10 +27,54 @@ public void setVolume(double volume) {
2327
nativeSetVolume(getNativeAudioTrack(), volume);
2428
}
2529

30+
/**
31+
* Adds an AudioTrackSink to the track. This callback is only
32+
* called for remote audio tracks.
33+
*
34+
* Repeated addSink calls will not add the sink multiple times.
35+
*/
36+
public void addSink(AudioTrackSink sink) {
37+
if (sink == null) {
38+
throw new IllegalArgumentException("The AudioTrackSink is not allowed to be null");
39+
}
40+
if (!sinks.containsKey(sink)) {
41+
final long nativeSink = nativeWrapSink(sink);
42+
sinks.put(sink, nativeSink);
43+
nativeAddSink(getNativeMediaStreamTrack(), nativeSink);
44+
}
45+
}
46+
47+
/**
48+
* Removes an AudioTrackSink from the track.
49+
*
50+
* If the AudioTrackSink was not attached to the track, this is a no-op.
51+
*/
52+
public void removeSink(AudioTrackSink sink) {
53+
final Long nativeSink = sinks.remove(sink);
54+
if (nativeSink != null) {
55+
nativeRemoveSink(getNativeMediaStreamTrack(), nativeSink);
56+
nativeFreeSink(nativeSink);
57+
}
58+
}
59+
60+
@Override
61+
public void dispose() {
62+
for (long nativeSink : sinks.values()) {
63+
nativeRemoveSink(getNativeMediaStreamTrack(), nativeSink);
64+
nativeFreeSink(nativeSink);
65+
}
66+
sinks.clear();
67+
super.dispose();
68+
}
69+
2670
/** Returns a pointer to webrtc::AudioTrackInterface. */
2771
long getNativeAudioTrack() {
2872
return getNativeMediaStreamTrack();
2973
}
3074

3175
private static native void nativeSetVolume(long track, double volume);
76+
private static native void nativeAddSink(long track, long nativeSink);
77+
private static native void nativeRemoveSink(long track, long nativeSink);
78+
private static native long nativeWrapSink(AudioTrackSink sink);
79+
private static native void nativeFreeSink(long sink);
3280
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Copyright 2023 The WebRTC project authors. All Rights Reserved.
3+
*
4+
* Use of this source code is governed by a BSD-style license
5+
* that can be found in the LICENSE file in the root of the source
6+
* tree. An additional intellectual property rights grant can be found
7+
* in the file PATENTS. All contributing project authors may
8+
* be found in the AUTHORS file in the root of the source tree.
9+
*/
10+
11+
package org.webrtc;
12+
13+
import java.nio.ByteBuffer;
14+
15+
/**
16+
* Java version of rtc::AudioTrackSinkInterface.
17+
*/
18+
public interface AudioTrackSink {
19+
/**
20+
* Implementations should copy the audio data into a local copy if they wish
21+
* to use the data after this function returns.
22+
*/
23+
@CalledByNative
24+
void onData(ByteBuffer audioData, int bitsPerSample, int sampleRate,
25+
int numberOfChannels, int numberOfFrames,
26+
long absoluteCaptureTimestampMs);
27+
}

sdk/android/api/org/webrtc/audio/JavaAudioDeviceModule.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ public static class Builder {
4242
private AudioTrackErrorCallback audioTrackErrorCallback;
4343
private AudioRecordErrorCallback audioRecordErrorCallback;
4444
private SamplesReadyCallback samplesReadyCallback;
45+
private PlaybackSamplesReadyCallback playbackSamplesReadyCallback;
4546
private AudioTrackStateCallback audioTrackStateCallback;
4647
private AudioRecordStateCallback audioRecordStateCallback;
4748
private boolean useHardwareAcousticEchoCanceler = isBuiltInAcousticEchoCancelerSupported();
@@ -140,6 +141,14 @@ public Builder setSamplesReadyCallback(SamplesReadyCallback samplesReadyCallback
140141
return this;
141142
}
142143

144+
/**
145+
* Set a callback to listen to the audio output passed to the AudioTrack.
146+
*/
147+
public Builder setPlaybackSamplesReadyCallback(PlaybackSamplesReadyCallback playbackSamplesReadyCallback) {
148+
this.playbackSamplesReadyCallback = playbackSamplesReadyCallback;
149+
return this;
150+
}
151+
143152
/**
144153
* Set a callback to retrieve information from the AudioTrack on when audio starts and stop.
145154
*/
@@ -258,7 +267,7 @@ public JavaAudioDeviceModule createAudioDeviceModule() {
258267
samplesReadyCallback, useHardwareAcousticEchoCanceler, useHardwareNoiseSuppressor);
259268
final WebRtcAudioTrack audioOutput =
260269
new WebRtcAudioTrack(context, audioManager, audioAttributes, audioTrackErrorCallback,
261-
audioTrackStateCallback, useLowLatency, enableVolumeLogger);
270+
audioTrackStateCallback, playbackSamplesReadyCallback, useLowLatency, enableVolumeLogger);
262271
return new JavaAudioDeviceModule(context, audioManager, audioInput, audioOutput,
263272
inputSampleRate, outputSampleRate, useStereoInput, useStereoOutput);
264273
}
@@ -325,6 +334,11 @@ public static interface SamplesReadyCallback {
325334
void onWebRtcAudioRecordSamplesReady(AudioSamples samples);
326335
}
327336

337+
/** Called when new audio samples are ready. This should only be set for debug purposes */
338+
public static interface PlaybackSamplesReadyCallback {
339+
void onWebRtcAudioTrackSamplesReady(AudioSamples samples);
340+
}
341+
328342
/* AudioTrack */
329343
// Audio playout/track error handler functions.
330344
public enum AudioTrackStartErrorCode {

sdk/android/src/java/org/webrtc/audio/WebRtcAudioTrack.java

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,14 @@
2020
import android.os.Process;
2121
import androidx.annotation.Nullable;
2222
import java.nio.ByteBuffer;
23+
import java.util.Arrays;
2324
import org.webrtc.CalledByNative;
2425
import org.webrtc.Logging;
2526
import org.webrtc.ThreadUtils;
2627
import org.webrtc.audio.JavaAudioDeviceModule.AudioTrackErrorCallback;
2728
import org.webrtc.audio.JavaAudioDeviceModule.AudioTrackStartErrorCode;
2829
import org.webrtc.audio.JavaAudioDeviceModule.AudioTrackStateCallback;
30+
import org.webrtc.audio.JavaAudioDeviceModule.PlaybackSamplesReadyCallback;
2931
import org.webrtc.audio.LowLatencyAudioBufferManager;
3032

3133
class WebRtcAudioTrack {
@@ -76,6 +78,7 @@ class WebRtcAudioTrack {
7678

7779
private final @Nullable AudioTrackErrorCallback errorCallback;
7880
private final @Nullable AudioTrackStateCallback stateCallback;
81+
private final @Nullable PlaybackSamplesReadyCallback audioSamplesReadyCallback;
7982

8083
/**
8184
* Audio thread which keeps calling AudioTrack.write() to stream audio.
@@ -129,6 +132,17 @@ public void run() {
129132
reportWebRtcAudioTrackError("AudioTrack.write failed: " + bytesWritten);
130133
}
131134
}
135+
136+
if (audioSamplesReadyCallback != null && keepAlive) {
137+
// Copy the entire byte buffer array. The start of the byteBuffer is not necessarily
138+
// at index 0.
139+
byte[] data = Arrays.copyOfRange(byteBuffer.array(), byteBuffer.arrayOffset(),
140+
sizeInBytes + byteBuffer.arrayOffset());
141+
audioSamplesReadyCallback.onWebRtcAudioTrackSamplesReady(
142+
new JavaAudioDeviceModule.AudioSamples(audioTrack.getAudioFormat(),
143+
audioTrack.getChannelCount(), audioTrack.getSampleRate(), data));
144+
}
145+
132146
if (useLowLatency) {
133147
bufferManager.maybeAdjustBufferSize(audioTrack);
134148
}
@@ -154,20 +168,21 @@ public void stopThread() {
154168
@CalledByNative
155169
WebRtcAudioTrack(Context context, AudioManager audioManager) {
156170
this(context, audioManager, null /* audioAttributes */, null /* errorCallback */,
157-
null /* stateCallback */, false /* useLowLatency */, true /* enableVolumeLogger */);
171+
null /* stateCallback */, null /* audioSamplesReadyCallback */, false /* useLowLatency */, true /* enableVolumeLogger */);
158172
}
159173

160174
WebRtcAudioTrack(Context context, AudioManager audioManager,
161175
@Nullable AudioAttributes audioAttributes, @Nullable AudioTrackErrorCallback errorCallback,
162-
@Nullable AudioTrackStateCallback stateCallback, boolean useLowLatency,
163-
boolean enableVolumeLogger) {
176+
@Nullable AudioTrackStateCallback stateCallback, @Nullable PlaybackSamplesReadyCallback audioSamplesReadyCallback,
177+
boolean useLowLatency, boolean enableVolumeLogger) {
164178
threadChecker.detachThread();
165179
this.context = context;
166180
this.audioManager = audioManager;
167181
this.audioAttributes = audioAttributes;
168182
this.errorCallback = errorCallback;
169183
this.stateCallback = stateCallback;
170184
this.volumeLogger = enableVolumeLogger ? new VolumeLogger(audioManager) : null;
185+
this.audioSamplesReadyCallback = audioSamplesReadyCallback;
171186
this.useLowLatency = useLowLatency;
172187
Logging.d(TAG, "ctor" + WebRtcAudioUtils.getThreadInfo());
173188
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright 2018 The WebRTC project authors. All Rights Reserved.
3+
*
4+
* Use of this source code is governed by a BSD-style license
5+
* that can be found in the LICENSE file in the root of the source
6+
* tree. An additional intellectual property rights grant can be found
7+
* in the file PATENTS. All contributing project authors may
8+
* be found in the AUTHORS file in the root of the source tree.
9+
*/
10+
11+
#include "sdk/android/src/jni/pc/audio_sink.h"
12+
13+
#include "sdk/android/generated_peerconnection_jni/AudioTrackSink_jni.h"
14+
15+
namespace webrtc {
16+
namespace jni {
17+
18+
AudioTrackSinkWrapper::AudioTrackSinkWrapper(JNIEnv* jni, const JavaRef<jobject>& j_sink)
19+
: j_sink_(jni, j_sink) {}
20+
21+
AudioTrackSinkWrapper::~AudioTrackSinkWrapper() {}
22+
23+
void AudioTrackSinkWrapper::OnData(
24+
const void* audio_data,
25+
int bits_per_sample,
26+
int sample_rate,
27+
size_t number_of_channels,
28+
size_t number_of_frames,
29+
absl::optional<int64_t> absolute_capture_timestamp_ms) {
30+
JNIEnv* jni = AttachCurrentThreadIfNeeded();
31+
int length = (bits_per_sample / 8) * number_of_channels * number_of_frames;
32+
ScopedJavaLocalRef<jobject> audio_buffer =
33+
NewDirectByteBuffer(jni, (void *) audio_data, length);
34+
Java_AudioTrackSink_onData(jni, j_sink_,
35+
audio_buffer, bits_per_sample, sample_rate, (int) number_of_channels, (int) number_of_frames, (absolute_capture_timestamp_ms ? absolute_capture_timestamp_ms.value() : 0));
36+
}
37+
38+
} // namespace jni
39+
} // namespace webrtc
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright 2018 The WebRTC project authors. All Rights Reserved.
3+
*
4+
* Use of this source code is governed by a BSD-style license
5+
* that can be found in the LICENSE file in the root of the source
6+
* tree. An additional intellectual property rights grant can be found
7+
* in the file PATENTS. All contributing project authors may
8+
* be found in the AUTHORS file in the root of the source tree.
9+
*/
10+
11+
#ifndef SDK_ANDROID_SRC_JNI_AUDIO_TRACK_SINK_H_
12+
#define SDK_ANDROID_SRC_JNI_AUDIO_TRACK_SINK_H_
13+
14+
#include <jni.h>
15+
16+
#include "api/media_stream_interface.h"
17+
#include "sdk/android/src/jni/jni_helpers.h"
18+
19+
namespace webrtc {
20+
namespace jni {
21+
22+
class AudioTrackSinkWrapper : public webrtc::AudioTrackSinkInterface {
23+
public:
24+
AudioTrackSinkWrapper(JNIEnv* jni, const JavaRef<jobject>& j_sink);
25+
~AudioTrackSinkWrapper() override;
26+
27+
private:
28+
void OnData(const void* audio_data,
29+
int bits_per_sample,
30+
int sample_rate,
31+
size_t number_of_channels,
32+
size_t number_of_frames,
33+
absl::optional<int64_t> absolute_capture_timestamp_ms) override;
34+
35+
const ScopedJavaGlobalRef<jobject> j_sink_;
36+
};
37+
38+
} // namespace jni
39+
} // namespace webrtc
40+
41+
#endif // SDK_ANDROID_SRC_JNI_AUDIO_TRACK_SINK_H_

sdk/android/src/jni/pc/audio_track.cc

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
*/
1010

1111
#include "api/media_stream_interface.h"
12+
#include "sdk/android/src/jni/pc/audio_sink.h"
13+
1214
#include "sdk/android/generated_peerconnection_jni/AudioTrack_jni.h"
1315

1416
namespace webrtc {
@@ -20,5 +22,29 @@ static void JNI_AudioTrack_SetVolume(JNIEnv*, jlong j_p, jdouble volume) {
2022
source->SetVolume(volume);
2123
}
2224

25+
static void JNI_AudioTrack_AddSink(JNIEnv* jni,
26+
jlong j_native_track,
27+
jlong j_native_sink) {
28+
reinterpret_cast<AudioTrackInterface*>(j_native_track)
29+
->AddSink(reinterpret_cast<webrtc::AudioTrackSinkInterface*>(j_native_sink));
30+
}
31+
32+
static void JNI_AudioTrack_RemoveSink(JNIEnv* jni,
33+
jlong j_native_track,
34+
jlong j_native_sink) {
35+
reinterpret_cast<AudioTrackInterface*>(j_native_track)
36+
->RemoveSink(reinterpret_cast<webrtc::AudioTrackSinkInterface*>(j_native_sink));
37+
}
38+
39+
static jlong JNI_AudioTrack_WrapSink(JNIEnv* jni,
40+
const JavaParamRef<jobject>& sink) {
41+
return jlongFromPointer(new AudioTrackSinkWrapper(jni, sink));
42+
}
43+
44+
static void JNI_AudioTrack_FreeSink(JNIEnv* jni, jlong j_native_sink) {
45+
delete reinterpret_cast<jni::AudioTrackSinkWrapper*>(j_native_sink);
46+
}
47+
48+
2349
} // namespace jni
2450
} // namespace webrtc

0 commit comments

Comments
 (0)