Skip to content

Commit b13e7fd

Browse files
committed
Latency detection for int16 streams too.
1 parent 125ad9b commit b13e7fd

File tree

4 files changed

+75
-39
lines changed

4 files changed

+75
-39
lines changed

app/src/main/cpp/AudioEngine.cpp

+48-18
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,8 @@ AudioEngine::onAudioReady(oboe::AudioStream *audioStream, void *audioData, int32
3838
const std::chrono::microseconds bufferBeginAtOutput = hostTime + latencyMuSec;
3939
const auto microsPerSample = 1e6 / mSampleRate;
4040

41-
4241
int64_t timeoutNanos = 100 * kNanosPerMillisecond;
4342

44-
4543
if (audioStream->getFormat() == oboe::AudioFormat::Float) {
4644

4745
// Logic: successive renders are added
@@ -82,16 +80,25 @@ AudioEngine::onAudioReady(oboe::AudioStream *audioStream, void *audioData, int32
8280
memset(static_cast<uint16_t *>(audioData), 0,
8381
sizeof(int16_t) * channelCount * numFrames);
8482

83+
if (mPlayStatus == playing || mPerformLatencyDetection) {
8584

86-
if (mPlayStatus == playing) {
87-
renderBarClick(static_cast<int16_t *>(audioData) + 1, channelCount,
85+
renderBarClick(static_cast<int16_t *>(audioData) , channelCount,
8886
numFrames, sessionState, bufferBeginAtOutput, microsPerSample);
8987

9088
for (int i = 0; i < channelCount; ++i) {
91-
9289
mOscillators[i].render(static_cast<int16_t *>(audioData) + i, channelCount,
9390
numFrames);
9491
}
92+
93+
if (mPerformLatencyDetection) {
94+
// The callback does a non-blocking read from the input stream placing the data into the internal input buffer.
95+
oboe::ErrorOrValue<int32_t> result = mRecStream->read(mInputBuffer_int16, numFrames, timeoutNanos);
96+
97+
if (!result) {
98+
LOGE("Error reading input stream %s", convertToText(result.error()));
99+
}
100+
processInput(static_cast<int16_t *>(audioData) , channelCount, numFrames);
101+
}
95102
}
96103
}
97104

@@ -147,23 +154,27 @@ void AudioEngine::renderBarClick(int16_t *buffer,
147154
ableton::Link::SessionState sessionState,
148155
std::chrono::microseconds bufferBeginAtOutput,
149156
double microsPerSample) {
150-
for (int i = 0, sampleIndex = 0; i < numFrames; i++) {
157+
for (int i = 0; i < numFrames; i++) {
151158

152159
const auto sampleHostTime = bufferBeginAtOutput + std::chrono::microseconds(llround(i * microsPerSample));
153-
int16_t sample = 0;
160+
154161
const double barPhase = sessionState.phaseAtTime(sampleHostTime, mQuantum);
155-
if ((barPhase - mLastBarPhase) < -(mQuantum/40)){
156-
// uncomment if you want to render a clik on each bar.
157-
sample = INT16_MAX * 1.0;
162+
163+
if ((barPhase - mLastBarPhase) < -(mQuantum/2)) {
158164
timeAtLastBar = sampleHostTime;
165+
for (int j = 0; j< channelStride; j++){
166+
buffer[i*channelStride + j] = INT16_MAX * 1.0;
167+
}
168+
mLatencySampleCount = 0;
169+
} else {
170+
mLatencySampleCount++;
159171
}
160-
buffer[sampleIndex] += sample;
161-
sampleIndex += channelStride;
162172
mLastBarPhase = barPhase;
163173
}
164174
}
165175

166176

177+
167178
// the buffer param refers to the output audio buffer. the input buffer is a private property.
168179
void AudioEngine::processInput(float *buffer,
169180
int32_t channelStride,
@@ -176,14 +187,28 @@ void AudioEngine::processInput(float *buffer,
176187

177188
// crude onset detection
178189
if ( (fabsf(mInputBuffer[i]) > 0.5) && !foundOnsetInBuffer){
179-
// LOGD("ONSET %i ", mLatencySampleCount);
180-
// LOGD("round trip latency %f", (double) mLatencySampleCount / mPlayStream->getSampleRate());
181-
182-
// set output latency to half round trip latency?
183-
mCurrentOutputLatencyMillis = ((double) mLatencySampleCount / mPlayStream->getSampleRate() * 1000) / 2.0;
190+
mDetectedOutputLatencyMillis = ((double) mLatencySampleCount / mPlayStream->getSampleRate() * 1000);
184191
foundOnsetInBuffer = true;
185192
}
193+
sampleIndex += channelStride;
194+
}
195+
}
186196

197+
// the buffer param refers to the output audio buffer. the input buffer is a private property.
198+
void AudioEngine::processInput(int16_t *buffer,
199+
int32_t channelStride,
200+
int32_t numFrames) {
201+
bool foundOnsetInBuffer = false;
202+
for (int i = 0, sampleIndex = 0; i < numFrames; i++) {
203+
204+
// Uncomment to copy input into output buffer
205+
// buffer[sampleIndex] += mInputBuffer[i];
206+
207+
// crude onset detection
208+
if ( (abs(mInputBuffer_int16[i]) > (INT16_MAX * 0.5)) && !foundOnsetInBuffer){
209+
mDetectedOutputLatencyMillis = ((double) mLatencySampleCount / mPlayStream->getSampleRate() * 1000);
210+
foundOnsetInBuffer = true;
211+
}
187212
sampleIndex += channelStride;
188213
}
189214
}
@@ -273,6 +298,7 @@ void AudioEngine::closeInputStream() {
273298
}
274299

275300
delete[] mInputBuffer;
301+
delete[] mInputBuffer_int16;
276302
}
277303
}
278304

@@ -305,6 +331,10 @@ void AudioEngine::createRecStream() {
305331
if (mInputBuffer == nullptr) {
306332
mInputBuffer = new float[mChannelCount * mRecStream->getBufferSizeInFrames()];
307333
}
334+
if (mInputBuffer_int16 == nullptr) {
335+
mInputBuffer_int16 = new int16_t[mChannelCount * mRecStream->getBufferSizeInFrames()];
336+
}
337+
308338
result = mRecStream->requestStart();
309339
if (result != oboe::Result::OK) {
310340
LOGE("Error starting stream. %s", oboe::convertToText(result));
@@ -457,5 +487,5 @@ void AudioEngine::detectLatency(bool flag){
457487
}
458488

459489
double AudioEngine::getLatencyMs(){
460-
return mCurrentOutputLatencyMillis;
490+
return mDetectedOutputLatencyMillis;
461491
}

app/src/main/cpp/AudioEngine.h

+5
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ class AudioEngine: oboe::AudioStreamCallback {
4545
int32_t mChannelCount;
4646
int32_t mFramesPerBurst;
4747
double mCurrentOutputLatencyMillis = 0;
48+
double mDetectedOutputLatencyMillis = 0;
4849
int32_t mBufferSizeSelection = kBufferSizeAutomatic;
4950
bool mIsLatencyDetectionSupported = false;
5051
oboe::AudioStream *mPlayStream;
@@ -81,9 +82,13 @@ class AudioEngine: oboe::AudioStreamCallback {
8182

8283
bool mPerformLatencyDetection = false;
8384
float* mInputBuffer = nullptr;
85+
int16_t * mInputBuffer_int16 = nullptr;
8486
void processInput(float *buffer,
8587
int32_t channelStride,
8688
int32_t numFrames);
89+
void processInput(int16_t *buffer,
90+
int32_t channelStride,
91+
int32_t numFrames);
8792
int mLatencySampleCount = 0;
8893

8994
// ABLETON LINK

app/src/main/java/com/jbloit/androidlinkaudio/MainActivity.kt

+3-4
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ class MainActivity : AppCompatActivity(), SeekBar.OnSeekBarChangeListener {
5757

5858
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
5959
AudioEngine.setLatency(progress)
60-
text_latencyMs.text = "Latency : $progress ms"
60+
text_latencyMs.text = "Output Latency : $progress ms \n Set to a little more than roundtrip latency"
6161
}
6262

6363

@@ -91,12 +91,11 @@ class MainActivity : AppCompatActivity(), SeekBar.OnSeekBarChangeListener {
9191
return check == PackageManager.PERMISSION_GRANTED
9292
}
9393

94-
9594
//region ANIMATION LOOP
9695
fun pollEngine(delta: Long){
9796

98-
val currentLatencyMs = AudioEngine.getLatency()
99-
text_latencyDetection.text = "Detected latency: $currentLatencyMs"
97+
val detectedLatencyMs = AudioEngine.getLatency()
98+
text_latencyDetection.text = "Detected round trip latency: $detectedLatencyMs ms"
10099
}
101100
fun startAnimationLoop(){
102101
animationLoop.setTimeListener(TimeAnimator.TimeListener({ animation, totalTime, deltaTime -> pollEngine(deltaTime)}))

app/src/main/res/layout/activity_main.xml

+19-17
Original file line numberDiff line numberDiff line change
@@ -54,48 +54,50 @@
5454
android:text="LINK " />
5555

5656
</LinearLayout>
57+
58+
5759
<LinearLayout
5860
android:layout_width="match_parent"
5961
android:layout_height="wrap_content"
60-
android:orientation="horizontal"
61-
android:textAlignment="center">
62+
android:orientation="horizontal">
63+
6264
<TextView
63-
android:id="@+id/text_latencyMs"
65+
android:id="@+id/text_latencyDetection"
6466
android:layout_width="wrap_content"
6567
android:layout_height="wrap_content"
6668
android:layout_weight="1"
67-
android:text="Latency ms"
69+
android:text="Latency detection"
6870
android:textAlignment="center" />
6971

70-
<SeekBar
71-
android:id="@+id/seekBar_latencyMs"
72+
<ToggleButton
73+
android:id="@+id/toggle_latencyDetection"
7274
android:layout_width="wrap_content"
7375
android:layout_height="wrap_content"
76+
android:layout_marginBottom="8dp"
77+
android:layout_marginTop="8dp"
7478
android:layout_weight="1"
75-
android:max="300" />
76-
79+
android:text="LATENCY DETECTION"/>
7780
</LinearLayout>
7881

7982
<LinearLayout
8083
android:layout_width="match_parent"
8184
android:layout_height="wrap_content"
82-
android:orientation="horizontal">
83-
85+
android:orientation="horizontal"
86+
android:textAlignment="center">
8487
<TextView
85-
android:id="@+id/text_latencyDetection"
88+
android:id="@+id/text_latencyMs"
8689
android:layout_width="wrap_content"
8790
android:layout_height="wrap_content"
8891
android:layout_weight="1"
89-
android:text="Latency detection"
92+
android:text="Output Latency ms"
9093
android:textAlignment="center" />
9194

92-
<ToggleButton
93-
android:id="@+id/toggle_latencyDetection"
95+
<SeekBar
96+
android:id="@+id/seekBar_latencyMs"
9497
android:layout_width="wrap_content"
9598
android:layout_height="wrap_content"
96-
android:layout_marginBottom="8dp"
97-
android:layout_marginTop="8dp"
9899
android:layout_weight="1"
99-
android:text="LATENCY DETECTION"/>
100+
android:max="300" />
101+
100102
</LinearLayout>
101103
</LinearLayout>

0 commit comments

Comments
 (0)