-
Notifications
You must be signed in to change notification settings - Fork 1.4k
/
ShadowAudioTrack.java
154 lines (134 loc) · 5.65 KB
/
ShadowAudioTrack.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
package org.robolectric.shadows;
import static android.media.AudioTrack.ERROR_BAD_VALUE;
import static android.media.AudioTrack.WRITE_BLOCKING;
import static android.media.AudioTrack.WRITE_NON_BLOCKING;
import static android.os.Build.VERSION_CODES.LOLLIPOP;
import static android.os.Build.VERSION_CODES.M;
import static android.os.Build.VERSION_CODES.N;
import static android.os.Build.VERSION_CODES.P;
import android.annotation.NonNull;
import android.media.AudioFormat;
import android.media.AudioTrack;
import android.media.AudioTrack.WriteMode;
import android.util.Log;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
import org.robolectric.annotation.Resetter;
/**
* Implementation of a couple methods in {@link AudioTrack}. Only a couple methods are supported,
* other methods are expected run through the real class. The two {@link WriteMode} are treated the
* same.
*/
@Implements(value = AudioTrack.class)
public class ShadowAudioTrack {
/**
* Listeners to be notified when data is written to an {@link AudioTrack} via {@link
* AudioTrack#write(ByteBuffer, int, int)}
*
* <p>Currently, only the data written through AudioTrack.write(ByteBuffer audioData, int
* sizeInBytes, int writeMode) will be reported.
*/
public interface OnAudioDataWrittenListener {
/**
* Called when data is written to {@link ShadowAudioTrack}.
*
* @param audioTrack The {@link ShadowAudioTrack} to which the data is written.
* @param audioData The data that is written to the {@link ShadowAudioTrack}.
* @param format The output format of the {@link ShadowAudioTrack}.
*/
void onAudioDataWritten(ShadowAudioTrack audioTrack, byte[] audioData, AudioFormat format);
}
protected static final int DEFAULT_MIN_BUFFER_SIZE = 1024;
private static final String TAG = "ShadowAudioTrack";
private static int minBufferSize = DEFAULT_MIN_BUFFER_SIZE;
private static final List<OnAudioDataWrittenListener> audioDataWrittenListeners =
new CopyOnWriteArrayList<>();
private int numBytesReceived;
@RealObject AudioTrack audioTrack;
/**
* In the real class, the minimum buffer size is estimated from audio sample rate and other
* factors. We do not provide such estimation in {@link #native_get_min_buff_size(int, int, int)},
* instead letting users set the minimum for the expected audio sample. Usually higher sample rate
* requires bigger buffer size.
*/
public static void setMinBufferSize(int bufferSize) {
minBufferSize = bufferSize;
}
@Implementation(minSdk = N, maxSdk = P)
protected static int native_get_FCC_8() {
// Return the value hard-coded in native code:
// https://cs.android.com/android/platform/superproject/+/android-7.1.1_r41:system/media/audio/include/system/audio.h;l=42;drc=57a4158dc4c4ce62bc6a2b8a0072ba43305548d4
return 8;
}
/** Returns a predefined or default minimum buffer size. Audio format and config are neglected. */
@Implementation
protected static int native_get_min_buff_size(
int sampleRateInHz, int channelConfig, int audioFormat) {
return minBufferSize;
}
/**
* Always return the number of bytes to write. This method returns immedidately even with {@link
* AudioTrack#WRITE_BLOCKING}
*/
@Implementation(minSdk = M)
protected final int native_write_byte(
byte[] audioData, int offsetInBytes, int sizeInBytes, int format, boolean isBlocking) {
return sizeInBytes;
}
/**
* Always return the number of bytes to write except with invalid parameters. Assumes AudioTrack
* is already initialized (object properly created). Do not block even if AudioTrack in offload
* mode is in STOPPING play state. This method returns immediately even with {@link
* AudioTrack#WRITE_BLOCKING}
*/
@Implementation(minSdk = LOLLIPOP)
protected int write(@NonNull ByteBuffer audioData, int sizeInBytes, @WriteMode int writeMode) {
if (writeMode != WRITE_BLOCKING && writeMode != WRITE_NON_BLOCKING) {
Log.e(TAG, "ShadowAudioTrack.write() called with invalid blocking mode");
return ERROR_BAD_VALUE;
}
if (sizeInBytes < 0 || sizeInBytes > audioData.remaining()) {
Log.e(TAG, "ShadowAudioTrack.write() called with invalid size (" + sizeInBytes + ") value");
return ERROR_BAD_VALUE;
}
byte[] receivedBytes = new byte[sizeInBytes];
audioData.get(receivedBytes);
numBytesReceived += sizeInBytes;
for (OnAudioDataWrittenListener listener : audioDataWrittenListeners) {
listener.onAudioDataWritten(this, receivedBytes, audioTrack.getFormat());
}
return sizeInBytes;
}
@Implementation
protected int getPlaybackHeadPosition() {
return numBytesReceived / audioTrack.getFormat().getFrameSizeInBytes();
}
@Implementation
protected void flush() {
numBytesReceived = 0;
}
/**
* Registers an {@link OnAudioDataWrittenListener} to the {@link ShadowAudioTrack}.
*
* @param listener The {@link OnAudioDataWrittenListener} to be registered.
*/
public static void addAudioDataListener(OnAudioDataWrittenListener listener) {
ShadowAudioTrack.audioDataWrittenListeners.add(listener);
}
/**
* Removes an {@link OnAudioDataWrittenListener} from the {@link ShadowAudioTrack}.
*
* @param listener The {@link OnAudioDataWrittenListener} to be removed.
*/
public static void removeAudioDataListener(OnAudioDataWrittenListener listener) {
ShadowAudioTrack.audioDataWrittenListeners.remove(listener);
}
@Resetter
public static void resetTest() {
audioDataWrittenListeners.clear();
}
}