Skip to content

Commit d56d05c

Browse files
authored
Merge pull request #2368 from hasinur010/tap2tone
Update Tap-to-Tone with filter and tone generator options
2 parents 0cac04d + b7b2df3 commit d56d05c

12 files changed

Lines changed: 335 additions & 57 deletions

apps/OboeTester/app/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3")
66

77
link_directories(${CMAKE_CURRENT_LIST_DIR}/..)
88

9-
# Increment this number when adding files to OboeTester => 111
9+
# Increment this number when adding files to OboeTester => 112
1010
# The change in this file will help Android Studio resync
1111
# and generate new build files that reference the new code.
1212
file(GLOB_RECURSE app_native_sources src/main/cpp/*)
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/*
2+
* Copyright 2026 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#include "BlipGenerator.h"
18+
19+
BlipGenerator::BlipGenerator()
20+
: output(*this, 1)
21+
, mSrcPhase(0.0f)
22+
, mPhaseIncr(0.0f)
23+
, mNumPendingPulseFrames(0)
24+
, mRequestCount(0)
25+
, mAcknowledgeCount(0) {
26+
27+
float incr = ((float)M_PI * 2.0f) / (float)NUM_WAVETABLE_SAMPLES;
28+
for(int i = 0; i < WAVETABLE_LENGTH; i++) {
29+
mWaveTable[i] = sinf(i * incr);
30+
}
31+
}
32+
33+
void BlipGenerator::setSampleRate(int sampleRate) {
34+
float fn = sampleRate / (float)NUM_WAVETABLE_SAMPLES;
35+
float fnInverse = 1.0f / fn;
36+
mPhaseIncr = 1000.0f * fnInverse;
37+
}
38+
39+
void BlipGenerator::reset() {
40+
FlowGraphNode::reset();
41+
mAcknowledgeCount.store(mRequestCount.load());
42+
mNumPendingPulseFrames = 0;
43+
mSrcPhase = 0.0f;
44+
}
45+
46+
void BlipGenerator::trigger() {
47+
mRequestCount++;
48+
}
49+
50+
int32_t BlipGenerator::onProcess(int numFrames) {
51+
float *buffer = output.getBuffer();
52+
53+
if (mRequestCount.load() > mAcknowledgeCount.load()) {
54+
mAcknowledgeCount++;
55+
mNumPendingPulseFrames = NUM_PULSE_FRAMES;
56+
}
57+
58+
if (mNumPendingPulseFrames <= 0) {
59+
for (int i = 0; i < numFrames; i++) {
60+
*buffer++ = 0.0f;
61+
}
62+
} else {
63+
for (int i = 0; i < numFrames; i++) {
64+
if (mNumPendingPulseFrames > 0) {
65+
while (mSrcPhase >= (float)NUM_WAVETABLE_SAMPLES) {
66+
mSrcPhase -= (float)NUM_WAVETABLE_SAMPLES;
67+
}
68+
69+
int srcIndex = (int)mSrcPhase;
70+
float delta = mSrcPhase - (float)srcIndex;
71+
float s0 = mWaveTable[srcIndex];
72+
float s1 = mWaveTable[srcIndex + 1];
73+
float value = s0 + ((s1 - s0) * delta);
74+
75+
*buffer++ = value;
76+
mSrcPhase += mPhaseIncr;
77+
mNumPendingPulseFrames--;
78+
} else {
79+
*buffer++ = 0.0f;
80+
}
81+
}
82+
}
83+
84+
return numFrames;
85+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright 2026 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#ifndef NATIVEOBOE_EXACTBLIPGENERATOR_H
18+
#define NATIVEOBOE_EXACTBLIPGENERATOR_H
19+
20+
#include <atomic>
21+
#include <math.h>
22+
#include "flowgraph/FlowGraphNode.h"
23+
24+
class BlipGenerator : public oboe::flowgraph::FlowGraphNode {
25+
public:
26+
BlipGenerator();
27+
virtual ~BlipGenerator() = default;
28+
29+
void setSampleRate(int sampleRate);
30+
int32_t onProcess(int numFrames) override;
31+
void trigger();
32+
void reset() override;
33+
34+
oboe::flowgraph::FlowGraphPortFloatOutput output;
35+
36+
private:
37+
static const int WAVETABLE_LENGTH = 2049;
38+
static const int NUM_WAVETABLE_SAMPLES = 2048; // LENGTH - 1
39+
40+
static const int NUM_PULSE_FRAMES = (int) (48000 * (1.0 / 16.0));
41+
42+
float mWaveTable[WAVETABLE_LENGTH];
43+
float mSrcPhase;
44+
float mPhaseIncr;
45+
int mNumPendingPulseFrames;
46+
47+
std::atomic<int> mRequestCount;
48+
std::atomic<int> mAcknowledgeCount;
49+
};
50+
51+
#endif // NATIVEOBOE_EXACTBLIPGENERATOR_H

apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -771,11 +771,16 @@ void ActivityTapToTone::configureAfterOpen() {
771771
mSawPingGenerator.setSampleRate(outputStream->getSampleRate());
772772
mSawPingGenerator.frequency.setValue(FREQUENCY_SAW_PING);
773773
mSawPingGenerator.amplitude.setValue(AMPLITUDE_SAW_PING);
774+
mBlipGenerator.setSampleRate(outputStream->getSampleRate());
774775

775-
if (mUseNoisePulse) {
776+
if (mUseToneGeneratorType == "Blip"){
777+
mBlipGenerator.output.connect(&(monoToMulti->input));
778+
} else if (mUseToneGeneratorType == "Saw"){
779+
mSawPingGenerator.output.connect(&(monoToMulti->input));
780+
} else if (mUseToneGeneratorType == "NoisePulse"){
776781
mNoisePulseGenerator.output.connect(&(monoToMulti->input));
777782
} else {
778-
mSawPingGenerator.output.connect(&(monoToMulti->input));
783+
mBlipGenerator.output.connect(&(monoToMulti->input));
779784
}
780785

781786
monoToMulti->output.connect(&(mSinkFloat.get()->input));

apps/OboeTester/app/src/main/cpp/NativeAudioContext.h

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
#include "OboeTools.h"
6161
#include "PlayRecordingCallback.h"
6262
#include "SawPingGenerator.h"
63+
#include "BlipGenerator.h"
6364

6465
// These must match order in strings.xml and in StreamConfiguration.java
6566
#define NATIVE_MODE_UNSPECIFIED 0
@@ -557,19 +558,26 @@ class ActivityTapToTone : public ActivityTestOutput {
557558
void configureAfterOpen() override;
558559

559560
void trigger() override {
560-
if (mUseNoisePulse) {
561+
if (mUseToneGeneratorType == "Blip"){
562+
mBlipGenerator.trigger();
563+
} else if (mUseToneGeneratorType == "Saw"){
564+
mSawPingGenerator.trigger();
565+
} else if (mUseToneGeneratorType == "NoisePulse"){
561566
mNoisePulseGenerator.trigger();
562567
} else {
563-
mSawPingGenerator.trigger();
568+
// By default use Blip
569+
mBlipGenerator.trigger();
564570
}
565571
}
566572

567-
void useNoisePulse(bool enabled) {
568-
mUseNoisePulse = enabled;
573+
void useToneGenerator(std::string type) {
574+
mUseToneGeneratorType = type;
575+
LOGD("Using %s tone generator\n", type.c_str());
569576
}
570577

571-
bool mUseNoisePulse;
578+
std::string mUseToneGeneratorType;
572579
SawPingGenerator mSawPingGenerator;
580+
BlipGenerator mBlipGenerator;
573581
NoisePulseGenerator mNoisePulseGenerator;
574582
};
575583

apps/OboeTester/app/src/main/cpp/jni-bridge.cpp

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1139,10 +1139,12 @@ Java_com_mobileer_oboetester_TestAudioActivity_setDuck(JNIEnv *env, jobject, jbo
11391139
}
11401140

11411141
JNIEXPORT void JNICALL
1142-
Java_com_mobileer_oboetester_TapToToneActivity_useNoisePulse(JNIEnv *env,
1143-
jclass clazz,
1144-
jboolean enabled) {
1145-
engine.mActivityTapToTone.useNoisePulse(enabled);
1142+
Java_com_mobileer_oboetester_TapToToneActivity_useToneGenerator(JNIEnv *env,
1143+
jobject,
1144+
jstring type) {
1145+
const char *typeStr = env->GetStringUTFChars(type, nullptr);
1146+
engine.mActivityTapToTone.useToneGenerator(typeStr);
1147+
env->ReleaseStringUTFChars(type, typeStr);
11461148
}
11471149

11481150
static TestErrorCallback sErrorCallbackTester;

apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TapLatencyAnalyser.java

Lines changed: 61 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,32 @@
1515
*/
1616
package com.mobileer.oboetester;
1717

18+
import android.util.Log;
1819
import java.util.ArrayList;
1920

2021
/**
2122
* Analyze a recording and extract edges for latency analysis.
2223
*/
2324
public class TapLatencyAnalyser {
25+
public static final String TAG = "TapLatencyAnalyser";
2426
public static final int TYPE_TAP = 0;
25-
float[] mHighPassBuffer;
27+
28+
public enum FilterType {
29+
AUTO,
30+
HIGH_PASS,
31+
AVERAGE
32+
}
33+
34+
private FilterType mFilterType = FilterType.AUTO;
35+
36+
private float[] mFilteredBuffer;
2637
float[] mFastBuffer;
2738
float[] mSlowBuffer;
2839
float[] mLowThresholdBuffer;
2940
float [] mArmedIndexes;
3041

42+
private boolean mAverageFilterUsedInAuto = false;
43+
3144
private float mDroop = 0.995f;
3245
private static final float EDGE_THRESHOLD = 0.01f;
3346
private static final float LOW_FRACTION = 0.5f;
@@ -51,20 +64,42 @@ public TapLatencyEvent(int type, int sampleIndex) {
5164
*/
5265
public TapLatencyEvent[] analyze(float[] buffer, int offset, int numSamples) {
5366
// Use high pass filter to remove rumble from air conditioners.
54-
mHighPassBuffer = new float[numSamples];
55-
highPassFilter(buffer, offset, numSamples, mHighPassBuffer);
56-
57-
float[] mAverageBuffer = new float[numSamples];
58-
averageFilter(mHighPassBuffer, numSamples, mAverageBuffer);
59-
mHighPassBuffer = mAverageBuffer;
60-
61-
// Apply envelope follower.
62-
float[] peakBuffer = new float[numSamples];
6367
mFastBuffer = new float[numSamples];
6468
mSlowBuffer = new float[numSamples];
6569
mLowThresholdBuffer = new float[numSamples];
6670
mArmedIndexes = new float[numSamples];
67-
fillPeakBuffer(mHighPassBuffer, 0, numSamples, peakBuffer);
71+
72+
mAverageFilterUsedInAuto = false;
73+
float[] highPassBuffer = new float[numSamples];
74+
highPassFilter(buffer, offset, numSamples, highPassBuffer);
75+
TapLatencyEvent[] eventsFromHighpass = applyEnvelopeFollowerAndScanForEdges(highPassBuffer, numSamples);
76+
77+
float[] avgFilteredBuffer = new float[numSamples];
78+
averageFilter(highPassBuffer, numSamples, avgFilteredBuffer);
79+
TapLatencyEvent[] eventsFromAvg = applyEnvelopeFollowerAndScanForEdges(avgFilteredBuffer, numSamples);
80+
81+
if (mFilterType == FilterType.HIGH_PASS){
82+
mFilteredBuffer = highPassBuffer;
83+
return eventsFromHighpass;
84+
}else if(mFilterType == FilterType.AVERAGE){
85+
mFilteredBuffer = avgFilteredBuffer;
86+
return eventsFromAvg;
87+
}else{
88+
if (eventsFromHighpass.length == 2) {
89+
mFilteredBuffer = highPassBuffer;
90+
return eventsFromHighpass;
91+
} else {
92+
mFilteredBuffer = avgFilteredBuffer;
93+
mAverageFilterUsedInAuto = true;
94+
return eventsFromAvg;
95+
}
96+
}
97+
}
98+
99+
public TapLatencyEvent[] applyEnvelopeFollowerAndScanForEdges(float[] buffer, int numSamples) {
100+
// Apply envelope follower.
101+
float[] peakBuffer = new float[numSamples];
102+
fillPeakBuffer(buffer, 0, numSamples, peakBuffer);
68103
// Look for two attacks.
69104
return scanForEdges(peakBuffer, numSamples);
70105
}
@@ -74,7 +109,7 @@ public TapLatencyEvent[] analyze(float[] buffer, int offset, int numSamples) {
74109
* High-pass filtered to emphasize high-frequency events such as edges.
75110
*/
76111
public float[] getFilteredBuffer() {
77-
return mHighPassBuffer;
112+
return mFilteredBuffer;
78113
}
79114

80115

@@ -93,6 +128,16 @@ public float[] getLowThresholdBuffer() {
93128
public float[] getArmedIndexes() {
94129
return mArmedIndexes;
95130
}
131+
132+
public void setFilterType(FilterType filterType) {
133+
mFilterType = filterType;
134+
Log.d(TAG, "setFilterType: " + filterType);
135+
}
136+
137+
public boolean getAverageFilterUsedInAuto() {
138+
return mAverageFilterUsedInAuto;
139+
}
140+
96141
// Based on https://en.wikipedia.org/wiki/High-pass_filter
97142
private void highPassFilter(
98143
float[] buffer, int offset, int numSamples, float[] highPassBuffer) {
@@ -114,6 +159,10 @@ private void highPassFilter(
114159
}
115160

116161
private void averageFilter(float[] buffer, int numSamples, float[] averageBuffer) {
162+
if (numSamples <= 0) {
163+
Log.e("TapLatencyAnalyser", "averageFilter: numSamples = " + numSamples);
164+
return;
165+
}
117166
double sum = 0.0;
118167
for (int i = 0; i < numSamples; i++) {
119168
sum += buffer[i];

0 commit comments

Comments
 (0)