Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/OboeTester/app/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3")

link_directories(${CMAKE_CURRENT_LIST_DIR}/..)

# Increment this number when adding files to OboeTester => 111
# Increment this number when adding files to OboeTester => 112
# The change in this file will help Android Studio resync
# and generate new build files that reference the new code.
file(GLOB_RECURSE app_native_sources src/main/cpp/*)
Expand Down
85 changes: 85 additions & 0 deletions apps/OboeTester/app/src/main/cpp/BlipGenerator.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
* Copyright 2026 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

#include "BlipGenerator.h"

BlipGenerator::BlipGenerator()
: output(*this, 1)
, mSrcPhase(0.0f)
, mPhaseIncr(0.0f)
, mNumPendingPulseFrames(0)
, mRequestCount(0)
, mAcknowledgeCount(0) {

float incr = ((float)M_PI * 2.0f) / (float)NUM_WAVETABLE_SAMPLES;
for(int i = 0; i < WAVETABLE_LENGTH; i++) {
mWaveTable[i] = sinf(i * incr);
}
}

void BlipGenerator::setSampleRate(int sampleRate) {
float fn = sampleRate / (float)NUM_WAVETABLE_SAMPLES;
float fnInverse = 1.0f / fn;
mPhaseIncr = 1000.0f * fnInverse;
}

void BlipGenerator::reset() {
FlowGraphNode::reset();
mAcknowledgeCount.store(mRequestCount.load());
mNumPendingPulseFrames = 0;
mSrcPhase = 0.0f;
}

void BlipGenerator::trigger() {
mRequestCount++;
}

int32_t BlipGenerator::onProcess(int numFrames) {
float *buffer = output.getBuffer();

if (mRequestCount.load() > mAcknowledgeCount.load()) {
mAcknowledgeCount++;
mNumPendingPulseFrames = NUM_PULSE_FRAMES;
}

if (mNumPendingPulseFrames <= 0) {
for (int i = 0; i < numFrames; i++) {
*buffer++ = 0.0f;
}
} else {
for (int i = 0; i < numFrames; i++) {
if (mNumPendingPulseFrames > 0) {
while (mSrcPhase >= (float)NUM_WAVETABLE_SAMPLES) {
mSrcPhase -= (float)NUM_WAVETABLE_SAMPLES;
}

int srcIndex = (int)mSrcPhase;
float delta = mSrcPhase - (float)srcIndex;
float s0 = mWaveTable[srcIndex];
float s1 = mWaveTable[srcIndex + 1];
float value = s0 + ((s1 - s0) * delta);

*buffer++ = value;
mSrcPhase += mPhaseIncr;
mNumPendingPulseFrames--;
} else {
*buffer++ = 0.0f;
}
}
}

return numFrames;
}
51 changes: 51 additions & 0 deletions apps/OboeTester/app/src/main/cpp/BlipGenerator.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright 2026 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

#ifndef NATIVEOBOE_EXACTBLIPGENERATOR_H
#define NATIVEOBOE_EXACTBLIPGENERATOR_H

#include <atomic>
#include <math.h>
#include "flowgraph/FlowGraphNode.h"

class BlipGenerator : public oboe::flowgraph::FlowGraphNode {
public:
BlipGenerator();
virtual ~BlipGenerator() = default;

void setSampleRate(int sampleRate);
int32_t onProcess(int numFrames) override;
void trigger();
void reset() override;

oboe::flowgraph::FlowGraphPortFloatOutput output;

private:
static const int WAVETABLE_LENGTH = 2049;
static const int NUM_WAVETABLE_SAMPLES = 2048; // LENGTH - 1

static const int NUM_PULSE_FRAMES = (int) (48000 * (1.0 / 16.0));

float mWaveTable[WAVETABLE_LENGTH];
float mSrcPhase;
float mPhaseIncr;
int mNumPendingPulseFrames;

std::atomic<int> mRequestCount;
std::atomic<int> mAcknowledgeCount;
};

#endif // NATIVEOBOE_EXACTBLIPGENERATOR_H
9 changes: 7 additions & 2 deletions apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -771,11 +771,16 @@ void ActivityTapToTone::configureAfterOpen() {
mSawPingGenerator.setSampleRate(outputStream->getSampleRate());
mSawPingGenerator.frequency.setValue(FREQUENCY_SAW_PING);
mSawPingGenerator.amplitude.setValue(AMPLITUDE_SAW_PING);
mBlipGenerator.setSampleRate(outputStream->getSampleRate());

if (mUseNoisePulse) {
if (mUseToneGeneratorType == "Blip"){
mBlipGenerator.output.connect(&(monoToMulti->input));
} else if (mUseToneGeneratorType == "Saw"){
mSawPingGenerator.output.connect(&(monoToMulti->input));
} else if (mUseToneGeneratorType == "NoisePulse"){
mNoisePulseGenerator.output.connect(&(monoToMulti->input));
} else {
mSawPingGenerator.output.connect(&(monoToMulti->input));
mBlipGenerator.output.connect(&(monoToMulti->input));
}

monoToMulti->output.connect(&(mSinkFloat.get()->input));
Expand Down
18 changes: 13 additions & 5 deletions apps/OboeTester/app/src/main/cpp/NativeAudioContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
#include "OboeTools.h"
#include "PlayRecordingCallback.h"
#include "SawPingGenerator.h"
#include "BlipGenerator.h"

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

void trigger() override {
if (mUseNoisePulse) {
if (mUseToneGeneratorType == "Blip"){
mBlipGenerator.trigger();
} else if (mUseToneGeneratorType == "Saw"){
mSawPingGenerator.trigger();
} else if (mUseToneGeneratorType == "NoisePulse"){
mNoisePulseGenerator.trigger();
} else {
mSawPingGenerator.trigger();
// By default use Blip
mBlipGenerator.trigger();
}
}

void useNoisePulse(bool enabled) {
mUseNoisePulse = enabled;
void useToneGenerator(std::string type) {
mUseToneGeneratorType = type;
LOGD("Using %s tone generator\n", type.c_str());
}

bool mUseNoisePulse;
std::string mUseToneGeneratorType;
SawPingGenerator mSawPingGenerator;
BlipGenerator mBlipGenerator;
NoisePulseGenerator mNoisePulseGenerator;
};

Expand Down
10 changes: 6 additions & 4 deletions apps/OboeTester/app/src/main/cpp/jni-bridge.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1139,10 +1139,12 @@ Java_com_mobileer_oboetester_TestAudioActivity_setDuck(JNIEnv *env, jobject, jbo
}

JNIEXPORT void JNICALL
Java_com_mobileer_oboetester_TapToToneActivity_useNoisePulse(JNIEnv *env,
jclass clazz,
jboolean enabled) {
engine.mActivityTapToTone.useNoisePulse(enabled);
Java_com_mobileer_oboetester_TapToToneActivity_useToneGenerator(JNIEnv *env,
jobject,
jstring type) {
const char *typeStr = env->GetStringUTFChars(type, nullptr);
engine.mActivityTapToTone.useToneGenerator(typeStr);
env->ReleaseStringUTFChars(type, typeStr);
}

static TestErrorCallback sErrorCallbackTester;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,32 @@
*/
package com.mobileer.oboetester;

import android.util.Log;
import java.util.ArrayList;

/**
* Analyze a recording and extract edges for latency analysis.
*/
public class TapLatencyAnalyser {
public static final String TAG = "TapLatencyAnalyser";
public static final int TYPE_TAP = 0;
float[] mHighPassBuffer;

public enum FilterType {
AUTO,
HIGH_PASS,
AVERAGE
}

private FilterType mFilterType = FilterType.AUTO;

private float[] mFilteredBuffer;
float[] mFastBuffer;
float[] mSlowBuffer;
float[] mLowThresholdBuffer;
float [] mArmedIndexes;

private boolean mAverageFilterUsedInAuto = false;

private float mDroop = 0.995f;
private static final float EDGE_THRESHOLD = 0.01f;
private static final float LOW_FRACTION = 0.5f;
Expand All @@ -51,20 +64,42 @@ public TapLatencyEvent(int type, int sampleIndex) {
*/
public TapLatencyEvent[] analyze(float[] buffer, int offset, int numSamples) {
// Use high pass filter to remove rumble from air conditioners.
mHighPassBuffer = new float[numSamples];
highPassFilter(buffer, offset, numSamples, mHighPassBuffer);

float[] mAverageBuffer = new float[numSamples];
averageFilter(mHighPassBuffer, numSamples, mAverageBuffer);
mHighPassBuffer = mAverageBuffer;

// Apply envelope follower.
float[] peakBuffer = new float[numSamples];
mFastBuffer = new float[numSamples];
mSlowBuffer = new float[numSamples];
mLowThresholdBuffer = new float[numSamples];
mArmedIndexes = new float[numSamples];
fillPeakBuffer(mHighPassBuffer, 0, numSamples, peakBuffer);

mAverageFilterUsedInAuto = false;
float[] highPassBuffer = new float[numSamples];
highPassFilter(buffer, offset, numSamples, highPassBuffer);
TapLatencyEvent[] eventsFromHighpass = applyEnvelopeFollowerAndScanForEdges(highPassBuffer, numSamples);

float[] avgFilteredBuffer = new float[numSamples];
averageFilter(highPassBuffer, numSamples, avgFilteredBuffer);
TapLatencyEvent[] eventsFromAvg = applyEnvelopeFollowerAndScanForEdges(avgFilteredBuffer, numSamples);

if (mFilterType == FilterType.HIGH_PASS){
mFilteredBuffer = highPassBuffer;
return eventsFromHighpass;
}else if(mFilterType == FilterType.AVERAGE){
mFilteredBuffer = avgFilteredBuffer;
return eventsFromAvg;
}else{
if (eventsFromHighpass.length == 2) {
mFilteredBuffer = highPassBuffer;
return eventsFromHighpass;
} else {
mFilteredBuffer = avgFilteredBuffer;
mAverageFilterUsedInAuto = true;
return eventsFromAvg;
}
}
}

public TapLatencyEvent[] applyEnvelopeFollowerAndScanForEdges(float[] buffer, int numSamples) {
// Apply envelope follower.
float[] peakBuffer = new float[numSamples];
fillPeakBuffer(buffer, 0, numSamples, peakBuffer);
// Look for two attacks.
return scanForEdges(peakBuffer, numSamples);
}
Expand All @@ -74,7 +109,7 @@ public TapLatencyEvent[] analyze(float[] buffer, int offset, int numSamples) {
* High-pass filtered to emphasize high-frequency events such as edges.
*/
public float[] getFilteredBuffer() {
return mHighPassBuffer;
return mFilteredBuffer;
}


Expand All @@ -93,6 +128,16 @@ public float[] getLowThresholdBuffer() {
public float[] getArmedIndexes() {
return mArmedIndexes;
}

public void setFilterType(FilterType filterType) {
mFilterType = filterType;
Log.d(TAG, "setFilterType: " + filterType);
}

public boolean getAverageFilterUsedInAuto() {
return mAverageFilterUsedInAuto;
}

// Based on https://en.wikipedia.org/wiki/High-pass_filter
private void highPassFilter(
float[] buffer, int offset, int numSamples, float[] highPassBuffer) {
Expand All @@ -114,6 +159,10 @@ private void highPassFilter(
}

private void averageFilter(float[] buffer, int numSamples, float[] averageBuffer) {
if (numSamples <= 0) {
Log.e("TapLatencyAnalyser", "averageFilter: numSamples = " + numSamples);
return;
}
double sum = 0.0;
for (int i = 0; i < numSamples; i++) {
sum += buffer[i];
Expand Down
Loading
Loading