From 18c9fc6e673b69a4e680b75d87e42173a74b2577 Mon Sep 17 00:00:00 2001 From: Robert Wu <85952307+robertwu1@users.noreply.github.com> Date: Tue, 13 Feb 2024 15:32:05 -0800 Subject: [PATCH] Merge main into 1.8-stable (#1962) --- CMakeLists.txt | 2 + apps/OboeTester/app/build.gradle | 4 +- .../app/src/main/cpp/FullDuplexAnalyzer.cpp | 2 +- .../src/main/cpp/InterpolatingDelayLine.cpp | 3 +- .../app/src/main/cpp/InterpolatingDelayLine.h | 2 - .../src/main/cpp/analyzer/BaseSineAnalyzer.h | 4 +- .../src/main/cpp/analyzer/DataPathAnalyzer.h | 2 +- .../src/main/cpp/analyzer/GlitchAnalyzer.h | 135 +++++++++++++----- .../src/main/cpp/analyzer/InfiniteRecording.h | 5 +- .../src/main/cpp/analyzer/LatencyAnalyzer.h | 105 +++++++++++--- .../src/main/cpp/analyzer/ManchesterEncoder.h | 5 +- .../app/src/main/cpp/jni-bridge.cpp | 50 ++++++- .../oboetester/DeviceReportActivity.java | 48 ++++++- .../oboetester/DynamicWorkloadActivity.java | 109 +++++++------- .../mobileer/oboetester/GlitchActivity.java | 15 ++ .../oboetester/ManualGlitchActivity.java | 46 +++++- .../oboetester/TestDataPathsActivity.java | 111 ++++++++++---- .../oboetester/TestDisconnectActivity.java | 97 ++++++++++--- .../oboetester/TestInputActivity.java | 2 +- .../oboetester/TestOutputActivityBase.java | 9 +- .../com/mobileer/oboetester/WaveformView.java | 17 ++- .../com/mobileer/oboetester/WorkloadView.java | 39 +++-- .../main/res/layout/activity_data_paths.xml | 13 ++ .../res/layout/activity_dynamic_workload.xml | 9 +- .../res/layout/activity_manual_glitches.xml | 26 +++- .../res/layout/activity_test_disconnect.xml | 2 +- .../app/src/main/res/values/strings.xml | 4 + apps/OboeTester/docs/AutomatedTesting.md | 1 + apps/OboeTester/docs/Usage.md | 5 + include/oboe/AudioStream.h | 7 + include/oboe/AudioStreamBuilder.h | 8 ++ include/oboe/AudioStreamCallback.h | 3 +- include/oboe/Version.h | 2 +- src/common/AudioStreamBuilder.cpp | 8 +- src/flowgraph/FlowgraphUtilities.h | 15 ++ src/flowgraph/SinkI8_24.cpp | 55 +++++++ src/flowgraph/SinkI8_24.h | 40 ++++++ src/flowgraph/SourceI8_24.cpp | 54 +++++++ src/flowgraph/SourceI8_24.h | 42 ++++++ .../resampler/MultiChannelResampler.cpp | 2 +- src/opensles/AudioOutputStreamOpenSLES.cpp | 12 +- 41 files changed, 890 insertions(+), 230 deletions(-) create mode 100644 src/flowgraph/SinkI8_24.cpp create mode 100644 src/flowgraph/SinkI8_24.h create mode 100644 src/flowgraph/SourceI8_24.cpp create mode 100644 src/flowgraph/SourceI8_24.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 47bd84675..ac798aac4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -45,10 +45,12 @@ set (oboe_sources src/flowgraph/SinkI16.cpp src/flowgraph/SinkI24.cpp src/flowgraph/SinkI32.cpp + src/flowgraph/SinkI8_24.cpp src/flowgraph/SourceFloat.cpp src/flowgraph/SourceI16.cpp src/flowgraph/SourceI24.cpp src/flowgraph/SourceI32.cpp + src/flowgraph/SourceI8_24.cpp src/flowgraph/resampler/IntegerRatio.cpp src/flowgraph/resampler/LinearResampler.cpp src/flowgraph/resampler/MultiChannelResampler.cpp diff --git a/apps/OboeTester/app/build.gradle b/apps/OboeTester/app/build.gradle index 428fcf1d5..5f9016007 100644 --- a/apps/OboeTester/app/build.gradle +++ b/apps/OboeTester/app/build.gradle @@ -6,8 +6,8 @@ android { applicationId = "com.mobileer.oboetester" minSdkVersion 23 targetSdkVersion 34 - versionCode 78 - versionName "2.5.7" + versionCode 81 + versionName "2.5.10" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" externalNativeBuild { cmake { diff --git a/apps/OboeTester/app/src/main/cpp/FullDuplexAnalyzer.cpp b/apps/OboeTester/app/src/main/cpp/FullDuplexAnalyzer.cpp index 04c7086f9..243c647c8 100644 --- a/apps/OboeTester/app/src/main/cpp/FullDuplexAnalyzer.cpp +++ b/apps/OboeTester/app/src/main/cpp/FullDuplexAnalyzer.cpp @@ -56,7 +56,7 @@ oboe::DataCallbackResult FullDuplexAnalyzer::onBothStreamsReadyFloat( inputFloat += inputStride; mRecording->write(buffer, 1); } - // Handle mismatch in in numFrames. + // Handle mismatch in numFrames. buffer[0] = 0.0f; // gap in output for (int i = numBoth; i < numInputFrames; i++) { buffer[1] = *inputFloat; diff --git a/apps/OboeTester/app/src/main/cpp/InterpolatingDelayLine.cpp b/apps/OboeTester/app/src/main/cpp/InterpolatingDelayLine.cpp index e69c71122..f5393c57b 100644 --- a/apps/OboeTester/app/src/main/cpp/InterpolatingDelayLine.cpp +++ b/apps/OboeTester/app/src/main/cpp/InterpolatingDelayLine.cpp @@ -14,7 +14,8 @@ * limitations under the License. */ -#include "common/OboeDebug.h" +#include + #include "InterpolatingDelayLine.h" InterpolatingDelayLine::InterpolatingDelayLine(int32_t delaySize) { diff --git a/apps/OboeTester/app/src/main/cpp/InterpolatingDelayLine.h b/apps/OboeTester/app/src/main/cpp/InterpolatingDelayLine.h index bb90f2407..b05e23fb6 100644 --- a/apps/OboeTester/app/src/main/cpp/InterpolatingDelayLine.h +++ b/apps/OboeTester/app/src/main/cpp/InterpolatingDelayLine.h @@ -21,8 +21,6 @@ #include #include -#include "oboe/Oboe.h" - /** * Monophonic delay line. */ diff --git a/apps/OboeTester/app/src/main/cpp/analyzer/BaseSineAnalyzer.h b/apps/OboeTester/app/src/main/cpp/analyzer/BaseSineAnalyzer.h index f2a2c5d5b..90836c0bc 100644 --- a/apps/OboeTester/app/src/main/cpp/analyzer/BaseSineAnalyzer.h +++ b/apps/OboeTester/app/src/main/cpp/analyzer/BaseSineAnalyzer.h @@ -38,7 +38,6 @@ class BaseSineAnalyzer : public LoopbackProcessor { : LoopbackProcessor() , mInfiniteRecording(64 * 1024) {} - virtual bool isOutputEnabled() { return true; } void setMagnitude(double magnitude) { @@ -185,7 +184,8 @@ class BaseSineAnalyzer : public LoopbackProcessor { } protected: - static constexpr int32_t kTargetGlitchFrequency = 1000; + // Try to get a prime period so the waveform plot changes every time. + static constexpr int32_t kTargetGlitchFrequency = 48000 / 113; int32_t mSinePeriod = 1; // this will be set before use double mInverseSinePeriod = 1.0; diff --git a/apps/OboeTester/app/src/main/cpp/analyzer/DataPathAnalyzer.h b/apps/OboeTester/app/src/main/cpp/analyzer/DataPathAnalyzer.h index 52cd28b50..953cf49ab 100644 --- a/apps/OboeTester/app/src/main/cpp/analyzer/DataPathAnalyzer.h +++ b/apps/OboeTester/app/src/main/cpp/analyzer/DataPathAnalyzer.h @@ -65,7 +65,7 @@ class DataPathAnalyzer : public BaseSineAnalyzer { if (transformSample(sample, mOutputPhase)) { // Analyze magnitude and phase on every period. - double diff = abs(calculatePhaseError(mPhaseOffset, mPreviousPhaseOffset)); + double diff = fabs(calculatePhaseError(mPhaseOffset, mPreviousPhaseOffset)); if (diff < mPhaseTolerance) { mMaxMagnitude = std::max(mMagnitude, mMaxMagnitude); } diff --git a/apps/OboeTester/app/src/main/cpp/analyzer/GlitchAnalyzer.h b/apps/OboeTester/app/src/main/cpp/analyzer/GlitchAnalyzer.h index 43aa0e643..dc06dec7a 100644 --- a/apps/OboeTester/app/src/main/cpp/analyzer/GlitchAnalyzer.h +++ b/apps/OboeTester/app/src/main/cpp/analyzer/GlitchAnalyzer.h @@ -50,10 +50,22 @@ class GlitchAnalyzer : public BaseSineAnalyzer { return mMagnitude; } + int getSinePeriod() const { + return mSinePeriod; + } + + float getPhaseOffset() const { + return mPhaseOffset; + } + int32_t getGlitchCount() const { return mGlitchCount; } + int32_t getGlitchLength() const { + return mGlitchLength; + } + int32_t getStateFrameCount(int state) const { return mStateFrameCounters[state]; } @@ -124,21 +136,22 @@ class GlitchAnalyzer : public BaseSineAnalyzer { result_code result = RESULT_OK; float sample = frameData[getInputChannel()]; - float peak = mPeakFollower.process(sample); - mInfiniteRecording.write(sample); // Force a periodic glitch to test the detector! - if (mForceGlitchDuration > 0) { + if (mForceGlitchDurationFrames > 0) { if (mForceGlitchCounter == 0) { - ALOGE("%s: force a glitch!!", __func__); - mForceGlitchCounter = getSampleRate(); - } else if (mForceGlitchCounter <= mForceGlitchDuration) { + ALOGE("%s: finish a glitch!!", __func__); + mForceGlitchCounter = kForceGlitchPeriod; + } else if (mForceGlitchCounter <= mForceGlitchDurationFrames) { // Force an abrupt offset. - sample += (sample > 0.0) ? -0.5f : 0.5f; + sample += (sample > 0.0) ? -kForceGlitchOffset : kForceGlitchOffset; } --mForceGlitchCounter; } + float peak = mPeakFollower.process(sample); + mInfiniteRecording.write(sample); + mStateFrameCounters[mState]++; // count how many frames we are in each state switch (mState) { @@ -178,8 +191,9 @@ class GlitchAnalyzer : public BaseSineAnalyzer { // ALOGD("%s() mag = %f, offset = %f, prev = %f", // __func__, mMagnitude, mPhaseOffset, mPreviousPhaseOffset); if (mMagnitude > mThreshold) { - if (abs(mPhaseOffset) < kMaxPhaseError) { + if (fabs(mPhaseOffset) < kMaxPhaseError) { mState = STATE_LOCKED; + mConsecutiveBadFrames = 0; // ALOGD("%5d: switch to STATE_LOCKED", mFrameCounter); } // Adjust mInputPhase to match measured phase @@ -196,11 +210,20 @@ class GlitchAnalyzer : public BaseSineAnalyzer { double diff = predicted - sample; double absDiff = fabs(diff); mMaxGlitchDelta = std::max(mMaxGlitchDelta, absDiff); - if (absDiff > mScaledTolerance) { - result = ERROR_GLITCHES; - onGlitchStart(); -// LOGI("diff glitch detected, absDiff = %g", absDiff); - } else { + if (absDiff > mScaledTolerance) { // bad frame + mConsecutiveBadFrames++; + mConsecutiveGoodFrames = 0; + LOGI("diff glitch frame #%d detected, absDiff = %g > %g", + mConsecutiveBadFrames, absDiff, mScaledTolerance); + if (mConsecutiveBadFrames > 0) { + result = ERROR_GLITCHES; + onGlitchStart(); + } + resetAccumulator(); + } else { // good frame + mConsecutiveBadFrames = 0; + mConsecutiveGoodFrames++; + mSumSquareSignal += predicted * predicted; mSumSquareNoise += diff * diff; @@ -208,12 +231,15 @@ class GlitchAnalyzer : public BaseSineAnalyzer { // for drift in the DRC or AGC. // Must be a multiple of the period or the calculation will not be accurate. if (transformSample(sample, mInputPhase)) { + // Adjust phase to account for sample rate drift. + mInputPhase += mPhaseOffset; + mMeanSquareNoise = mSumSquareNoise * mInverseSinePeriod; mMeanSquareSignal = mSumSquareSignal * mInverseSinePeriod; mSumSquareNoise = 0.0; mSumSquareSignal = 0.0; - if (abs(mPhaseOffset) > kMaxPhaseError) { + if (fabs(mPhaseOffset) > kMaxPhaseError) { result = ERROR_GLITCHES; onGlitchStart(); ALOGD("phase glitch detected, phaseOffset = %g", mPhaseOffset); @@ -229,22 +255,25 @@ class GlitchAnalyzer : public BaseSineAnalyzer { case STATE_GLITCHING: { // Predict next sine value - mGlitchLength++; double predicted = sinf(mInputPhase) * mMagnitude; double diff = predicted - sample; double absDiff = fabs(diff); mMaxGlitchDelta = std::max(mMaxGlitchDelta, absDiff); - if (absDiff < mScaledTolerance) { // close enough? - // If we get a full sine period of non-glitch samples in a row then consider the glitch over. + if (absDiff > mScaledTolerance) { // bad frame + mConsecutiveBadFrames++; + mConsecutiveGoodFrames = 0; + mGlitchLength++; + if (mGlitchLength > maxMeasurableGlitchLength()) { + onGlitchTerminated(); + } + } else { // good frame + mConsecutiveBadFrames = 0; + mConsecutiveGoodFrames++; + // If we get a full sine period of good samples in a row then consider the glitch over. // We don't want to just consider a zero crossing the end of a glitch. - if (mNonGlitchCount++ > mSinePeriod) { + if (mConsecutiveGoodFrames > mSinePeriod) { onGlitchEnd(); } - } else { - mNonGlitchCount = 0; - if (mGlitchLength > (4 * mSinePeriod)) { - relock(); - } } incrementInputPhase(); } break; @@ -258,6 +287,8 @@ class GlitchAnalyzer : public BaseSineAnalyzer { return result; } + int maxMeasurableGlitchLength() const { return 2 * mSinePeriod; } + // advance and wrap phase void incrementInputPhase() { mInputPhase += mPhaseIncrement; @@ -269,16 +300,29 @@ class GlitchAnalyzer : public BaseSineAnalyzer { bool isOutputEnabled() override { return mState != STATE_IDLE; } void onGlitchStart() { - mGlitchCount++; -// ALOGD("%5d: STARTED a glitch # %d", mFrameCounter, mGlitchCount); mState = STATE_GLITCHING; mGlitchLength = 1; - mNonGlitchCount = 0; mLastGlitchPosition = mInfiniteRecording.getTotalWritten(); + ALOGD("%5d: STARTED a glitch # %d, pos = %5d", + mFrameCounter, mGlitchCount, (int)mLastGlitchPosition); + ALOGD("glitch mSinePeriod = %d", mSinePeriod); + } + + /** + * Give up waiting for a glitch to end and try to resync. + */ + void onGlitchTerminated() { + mGlitchCount++; + ALOGD("%5d: TERMINATED a glitch # %d, length = %d", mFrameCounter, mGlitchCount, mGlitchLength); + // We don't know how long the glitch really is so set the length to -1. + mGlitchLength = -1; + mState = STATE_WAITING_FOR_LOCK; + resetAccumulator(); } void onGlitchEnd() { -// ALOGD("%5d: ENDED a glitch # %d, length = %d", mFrameCounter, mGlitchCount, mGlitchLength); + mGlitchCount++; + ALOGD("%5d: ENDED a glitch # %d, length = %d", mFrameCounter, mGlitchCount, mGlitchLength); mState = STATE_LOCKED; resetAccumulator(); } @@ -288,12 +332,6 @@ class GlitchAnalyzer : public BaseSineAnalyzer { BaseSineAnalyzer::resetAccumulator(); } - void relock() { -// ALOGD("relock: %d because of a very long %d glitch", mFrameCounter, mGlitchLength); - mState = STATE_WAITING_FOR_LOCK; - resetAccumulator(); - } - void reset() override { BaseSineAnalyzer::reset(); mState = STATE_IDLE; @@ -303,6 +341,7 @@ class GlitchAnalyzer : public BaseSineAnalyzer { void prepareToTest() override { BaseSineAnalyzer::prepareToTest(); mGlitchCount = 0; + mGlitchLength = 0; mMaxGlitchDelta = 0.0; for (int i = 0; i < NUM_STATES; i++) { mStateFrameCounters[i] = 0; @@ -310,7 +349,26 @@ class GlitchAnalyzer : public BaseSineAnalyzer { } int32_t getLastGlitch(float *buffer, int32_t length) { - return mInfiniteRecording.readFrom(buffer, mLastGlitchPosition - 32, length); + const int margin = mSinePeriod; + int32_t numSamples = mInfiniteRecording.readFrom(buffer, + mLastGlitchPosition - margin, + length); + ALOGD("%s: glitch at %d, edge = %7.4f, %7.4f, %7.4f", + __func__, (int)mLastGlitchPosition, + buffer[margin - 1], buffer[margin], buffer[margin+1]); + return numSamples; + } + + int32_t getRecentSamples(float *buffer, int32_t length) { + int firstSample = mInfiniteRecording.getTotalWritten() - length; + int32_t numSamples = mInfiniteRecording.readFrom(buffer, + firstSample, + length); + return numSamples; + } + + void setForcedGlitchDuration(int frames) { + mForceGlitchDurationFrames = frames; } private: @@ -345,13 +403,16 @@ class GlitchAnalyzer : public BaseSineAnalyzer { double mInputPhase = 0.0; double mMaxGlitchDelta = 0.0; int32_t mGlitchCount = 0; - int32_t mNonGlitchCount = 0; + int32_t mConsecutiveBadFrames = 0; + int32_t mConsecutiveGoodFrames = 0; int32_t mGlitchLength = 0; int mDownCounter = IDLE_FRAME_COUNT; int32_t mFrameCounter = 0; - int32_t mForceGlitchDuration = 0; // if > 0 then force a glitch for debugging - int32_t mForceGlitchCounter = 4 * 48000; // count down and trigger at zero + int32_t mForceGlitchDurationFrames = 0; // if > 0 then force a glitch for debugging + static constexpr int32_t kForceGlitchPeriod = 2 * 48000; // How often we glitch + static constexpr float kForceGlitchOffset = 0.20f; + int32_t mForceGlitchCounter = kForceGlitchPeriod; // count down and trigger at zero // measure background noise continuously as a deviation from the expected signal double mSumSquareSignal = 0.0; diff --git a/apps/OboeTester/app/src/main/cpp/analyzer/InfiniteRecording.h b/apps/OboeTester/app/src/main/cpp/analyzer/InfiniteRecording.h index c02c00257..c12348c0f 100644 --- a/apps/OboeTester/app/src/main/cpp/analyzer/InfiniteRecording.h +++ b/apps/OboeTester/app/src/main/cpp/analyzer/InfiniteRecording.h @@ -34,6 +34,7 @@ class InfiniteRecording { int32_t readFrom(T *buffer, size_t position, size_t count) { const size_t maxPosition = mWritten.load(); position = std::min(position, maxPosition); + size_t numToRead = std::min(count, mMaxSamples); numToRead = std::min(numToRead, maxPosition - position); if (numToRead == 0) return 0; @@ -61,7 +62,7 @@ class InfiniteRecording { private: std::unique_ptr mData; - std::atomic mWritten{0}; - const size_t mMaxSamples; + std::atomic mWritten{0}; + const size_t mMaxSamples; }; #endif //OBOETESTER_INFINITE_RECORDING_H diff --git a/apps/OboeTester/app/src/main/cpp/analyzer/LatencyAnalyzer.h b/apps/OboeTester/app/src/main/cpp/analyzer/LatencyAnalyzer.h index b920c89d5..19f6cb11d 100644 --- a/apps/OboeTester/app/src/main/cpp/analyzer/LatencyAnalyzer.h +++ b/apps/OboeTester/app/src/main/cpp/analyzer/LatencyAnalyzer.h @@ -48,6 +48,9 @@ #define LOOPBACK_RESULT_TAG "RESULT: " +// Enable or disable the optimized latency calculation. +#define USE_FAST_LATENCY_CALCULATION 1 + static constexpr int32_t kDefaultSampleRate = 48000; static constexpr int32_t kMillisPerSecond = 1000; // by definition static constexpr int32_t kMaxLatencyMillis = 1000; // arbitrary and generous @@ -69,13 +72,14 @@ struct LatencyReport { static float calculateNormalizedCorrelation(const float *a, const float *b, - int windowSize) { + int windowSize, + int stride) { float correlation = 0.0; float sumProducts = 0.0; float sumSquares = 0.0; // Correlate a against b. - for (int i = 0; i < windowSize; i++) { + for (int i = 0; i < windowSize; i += stride) { float s1 = a[i]; float s2 = b[i]; // Use a normalized cross-correlation. @@ -204,7 +208,7 @@ class AudioRecording float normalize(float target) { float maxValue = 1.0e-9f; for (int i = 0; i < mFrameCounter; i++) { - maxValue = std::max(maxValue, abs(mData[i])); + maxValue = std::max(maxValue, fabsf(mData[i])); } float gain = target / maxValue; for (int i = 0; i < mFrameCounter; i++) { @@ -220,32 +224,46 @@ class AudioRecording int32_t mSampleRate = kDefaultSampleRate; // common default }; -static int measureLatencyFromPulse(AudioRecording &recorded, - AudioRecording &pulse, - LatencyReport *report) { - +/** + * Find latency using cross correlation in window of the recorded audio. + * The stride is used to skip over samples and reduce the CPU load. + */ +static int measureLatencyFromPulsePartial(AudioRecording &recorded, + int32_t recordedOffset, + int32_t recordedWindowSize, + AudioRecording &pulse, + LatencyReport *report, + int32_t stride) { report->reset(); - int numCorrelations = recorded.size() - pulse.size(); + if (recordedOffset + recordedWindowSize + pulse.size() > recorded.size()) { + ALOGE("%s() tried to correlate past end of recording, recordedOffset = %d frames\n", + __func__, recordedOffset); + return -3; + } + + int32_t numCorrelations = recordedWindowSize / stride; if (numCorrelations < 10) { - ALOGE("%s() recording too small = %d frames\n", __func__, recorded.size()); + ALOGE("%s() recording too small = %d frames, numCorrelations = %d\n", + __func__, recorded.size(), numCorrelations); return -1; } std::unique_ptr correlations= std::make_unique(numCorrelations); // Correlate pulse against the recorded data. - for (int i = 0; i < numCorrelations; i++) { - float correlation = calculateNormalizedCorrelation(&recorded.getData()[i], + for (int32_t i = 0; i < numCorrelations; i++) { + const int32_t index = (i * stride) + recordedOffset; + float correlation = calculateNormalizedCorrelation(&recorded.getData()[index], &pulse.getData()[0], - pulse.size()); + pulse.size(), + stride); correlations[i] = correlation; } - // Find highest peak in correlation array. float peakCorrelation = 0.0; - int peakIndex = -1; - for (int i = 0; i < numCorrelations; i++) { - float value = abs(correlations[i]); + int32_t peakIndex = -1; + for (int32_t i = 0; i < numCorrelations; i++) { + float value = fabsf(correlations[i]); if (value > peakCorrelation) { peakCorrelation = value; peakIndex = i; @@ -258,21 +276,64 @@ static int measureLatencyFromPulse(AudioRecording &recorded, #if 0 // Dump correlation data for charting. else { - const int margin = 50; - int startIndex = std::max(0, peakIndex - margin); - int endIndex = std::min(numCorrelations - 1, peakIndex + margin); - for (int index = startIndex; index < endIndex; index++) { + const int32_t margin = 50; + int32_t startIndex = std::max(0, peakIndex - margin); + int32_t endIndex = std::min(numCorrelations - 1, peakIndex + margin); + for (int32_t index = startIndex; index < endIndex; index++) { ALOGD("Correlation, %d, %f", index, correlations[index]); } } #endif - report->latencyInFrames = peakIndex; + report->latencyInFrames = recordedOffset + (peakIndex * stride); report->correlation = peakCorrelation; return 0; } +#if USE_FAST_LATENCY_CALCULATION +static int measureLatencyFromPulse(AudioRecording &recorded, + AudioRecording &pulse, + LatencyReport *report) { + const int32_t coarseStride = 16; + const int32_t fineWindowSize = coarseStride * 8; + const int32_t fineStride = 1; + LatencyReport courseReport; + courseReport.reset(); + // Do a rough search, skipping over most of the samples. + int result = measureLatencyFromPulsePartial(recorded, + 0, // recordedOffset, + recorded.size() - pulse.size(), + pulse, + &courseReport, + coarseStride); + if (result != 0) { + return result; + } + // Now do a fine resolution search near the coarse latency result. + int32_t recordedOffset = std::max(0, courseReport.latencyInFrames - (fineWindowSize / 2)); + result = measureLatencyFromPulsePartial(recorded, + recordedOffset, + fineWindowSize, + pulse, + report, + fineStride ); + return result; +} +#else +// TODO - When we are confident of the new code we can remove this old code. +static int measureLatencyFromPulse(AudioRecording &recorded, + AudioRecording &pulse, + LatencyReport *report) { + return measureLatencyFromPulsePartial(recorded, + 0, + recorded.size() - pulse.size(), + pulse, + report, + 1 ); +} +#endif + // ==================================================================================== class LoopbackProcessor { public: @@ -514,7 +575,7 @@ class PulseLatencyAnalyzer : public LatencyAnalyzer { << latencyMillis << "\n"; report << LOOPBACK_RESULT_TAG "latency.confidence = " << std::setw(8) << getMeasuredConfidence() << "\n"; - report << LOOPBACK_RESULT_TAG "latency.correlation = " << std::setw(8) + report << LOOPBACK_RESULT_TAG "latency.correlation = " << std::setw(8) << getMeasuredCorrelation() << "\n"; } mState = STATE_DONE; diff --git a/apps/OboeTester/app/src/main/cpp/analyzer/ManchesterEncoder.h b/apps/OboeTester/app/src/main/cpp/analyzer/ManchesterEncoder.h index 0a4bd5b26..af1c84304 100644 --- a/apps/OboeTester/app/src/main/cpp/analyzer/ManchesterEncoder.h +++ b/apps/OboeTester/app/src/main/cpp/analyzer/ManchesterEncoder.h @@ -45,13 +45,13 @@ class ManchesterEncoder { /** * This will be called when the next byte is needed. - * @return + * @return next byte */ virtual uint8_t onNextByte() = 0; /** * Generate the next floating point sample. - * @return + * @return next float */ virtual float nextFloat() { advanceSample(); @@ -66,7 +66,6 @@ class ManchesterEncoder { /** * This will be called when a new bit is ready to be encoded. * It can be used to prepare the encoded samples. - * @param current */ virtual void onNextBit(bool /* current */) {}; diff --git a/apps/OboeTester/app/src/main/cpp/jni-bridge.cpp b/apps/OboeTester/app/src/main/cpp/jni-bridge.cpp index 674255ce8..faa1d3259 100644 --- a/apps/OboeTester/app/src/main/cpp/jni-bridge.cpp +++ b/apps/OboeTester/app/src/main/cpp/jni-bridge.cpp @@ -753,10 +753,22 @@ Java_com_mobileer_oboetester_AnalyzerActivity_getResetCount(JNIEnv *env, // ========================================================================== JNIEXPORT jint JNICALL Java_com_mobileer_oboetester_GlitchActivity_getGlitchCount(JNIEnv *env, - jobject instance) { + jobject instance) { return engine.mActivityGlitches.getGlitchAnalyzer()->getGlitchCount(); } +JNIEXPORT jint JNICALL +Java_com_mobileer_oboetester_GlitchActivity_getGlitchLength(JNIEnv *env, + jobject instance) { + return engine.mActivityGlitches.getGlitchAnalyzer()->getGlitchLength(); +} + +JNIEXPORT double JNICALL +Java_com_mobileer_oboetester_GlitchActivity_getPhase(JNIEnv *env, + jobject instance) { + return engine.mActivityGlitches.getGlitchAnalyzer()->getPhaseOffset(); +} + JNIEXPORT jint JNICALL Java_com_mobileer_oboetester_GlitchActivity_getStateFrameCount(JNIEnv *env, jobject instance, @@ -782,6 +794,12 @@ Java_com_mobileer_oboetester_GlitchActivity_getSineAmplitude(JNIEnv *env, return engine.mActivityGlitches.getGlitchAnalyzer()->getSineAmplitude(); } +JNIEXPORT jint JNICALL +Java_com_mobileer_oboetester_GlitchActivity_getSinePeriod(JNIEnv *env, + jobject instance) { + return engine.mActivityGlitches.getGlitchAnalyzer()->getSinePeriod(); +} + JNIEXPORT jdouble JNICALL Java_com_mobileer_oboetester_TestDataPathsActivity_getMagnitude(JNIEnv *env, jobject instance) { @@ -794,12 +812,6 @@ Java_com_mobileer_oboetester_TestDataPathsActivity_getMaxMagnitude(JNIEnv *env, return engine.mActivityDataPath.getDataPathAnalyzer()->getMaxMagnitude(); } -JNIEXPORT jdouble JNICALL -Java_com_mobileer_oboetester_TestDataPathsActivity_getPhase(JNIEnv *env, - jobject instance) { - return engine.mActivityDataPath.getDataPathAnalyzer()->getPhaseOffset(); -} - JNIEXPORT void JNICALL Java_com_mobileer_oboetester_GlitchActivity_setTolerance(JNIEnv *env, jobject instance, @@ -809,6 +821,15 @@ Java_com_mobileer_oboetester_GlitchActivity_setTolerance(JNIEnv *env, } } +JNIEXPORT void JNICALL +Java_com_mobileer_oboetester_GlitchActivity_setForcedGlitchDuration(JNIEnv *env, + jobject instance, + jint frames) { + if (engine.mActivityGlitches.getGlitchAnalyzer()) { + engine.mActivityGlitches.getGlitchAnalyzer()->setForcedGlitchDuration(frames); + } +} + JNIEXPORT void JNICALL Java_com_mobileer_oboetester_GlitchActivity_setInputChannelNative(JNIEnv *env, jobject instance, @@ -848,6 +869,21 @@ Java_com_mobileer_oboetester_ManualGlitchActivity_getGlitch(JNIEnv *env, jobject return numSamples; } +JNIEXPORT jint JNICALL +Java_com_mobileer_oboetester_ManualGlitchActivity_getRecentSamples(JNIEnv *env, jobject instance, + jfloatArray waveform_) { + float *waveform = env->GetFloatArrayElements(waveform_, nullptr); + jsize length = env->GetArrayLength(waveform_); + jsize numSamples = 0; + auto *analyzer = engine.mActivityGlitches.getGlitchAnalyzer(); + if (analyzer) { + numSamples = analyzer->getRecentSamples(waveform, length); + } + + env->ReleaseFloatArrayElements(waveform_, waveform, 0); + return numSamples; +} + JNIEXPORT void JNICALL Java_com_mobileer_oboetester_TestAudioActivity_setDefaultAudioValues(JNIEnv *env, jclass clazz, jint audio_manager_sample_rate, diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/DeviceReportActivity.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/DeviceReportActivity.java index e0ef42608..311462369 100644 --- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/DeviceReportActivity.java +++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/DeviceReportActivity.java @@ -26,6 +26,8 @@ import android.media.AudioDeviceInfo; import android.media.AudioManager; import android.media.MicrophoneInfo; +import android.media.midi.MidiDeviceInfo; +import android.media.midi.MidiManager; import android.os.Build; import android.os.Bundle; import android.util.Log; @@ -34,15 +36,13 @@ import android.widget.TextView; import android.widget.Toast; -import androidx.annotation.NonNull; - import com.mobileer.audio_device.AudioDeviceInfoConverter; import java.io.IOException; import java.util.Collection; import java.util.HashMap; import java.util.List; -import java.util.Map; +import java.util.Set; /** * Print a report of all the available audio devices. @@ -73,6 +73,7 @@ public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) { private TextView mAutoTextView; private AudioManager mAudioManager; private UsbManager mUsbManager; + private MidiManager mMidiManager; @Override protected void onCreate(Bundle savedInstanceState) { @@ -81,6 +82,7 @@ protected void onCreate(Bundle savedInstanceState) { mAutoTextView = (TextView) findViewById(R.id.text_log_device_report); mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); mUsbManager = (UsbManager) getSystemService(Context.USB_SERVICE); + mMidiManager = (MidiManager) getSystemService(Context.MIDI_SERVICE); } @Override @@ -142,6 +144,7 @@ private void reportDeviceInfo(Collection devices) { } report.append(reportAllMicrophones()); report.append(reportUsbDevices()); + report.append(reportMidiDevices()); log(report.toString()); } @@ -173,6 +176,45 @@ public String reportUsbDevices() { return report.toString(); } + public String reportMidiDevices() { + StringBuffer report = new StringBuffer(); + report.append("\n############################"); + report.append("\nMidi Device Report:\n"); + try { + MidiDeviceInfo[] midiDeviceInfos = mMidiManager.getDevices(); + for (MidiDeviceInfo midiDeviceInfo : midiDeviceInfos) { + report.append("\n==== MIDI Device ========= " + midiDeviceInfo.getId()); + addMidiDeviceInfoToDeviceReport(midiDeviceInfo, report); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + Set umpDeviceInfos = + mMidiManager.getDevicesForTransport(MidiManager.TRANSPORT_UNIVERSAL_MIDI_PACKETS); + for (MidiDeviceInfo midiDeviceInfo : umpDeviceInfos) { + report.append("\n==== UMP Device ========= " + midiDeviceInfo.getId()); + addMidiDeviceInfoToDeviceReport(midiDeviceInfo, report); + } + } + } catch (Exception e) { + Log.e(TestAudioActivity.TAG, "Caught ", e); + showErrorToast(e.getMessage()); + report.append("\nERROR: " + e.getMessage() + "\n"); + } + return report.toString(); + } + + private void addMidiDeviceInfoToDeviceReport(MidiDeviceInfo midiDeviceInfo, + StringBuffer report){ + report.append("\nInput Count : " + midiDeviceInfo.getInputPortCount()); + report.append("\nOutput Count : " + midiDeviceInfo.getOutputPortCount()); + report.append("\nType : " + midiDeviceInfo.getType()); + report.append("\nIs Private : " + midiDeviceInfo.isPrivate()); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + report.append("\nDefault Protocol : " + midiDeviceInfo.getDefaultProtocol()); + } + report.append("\n" + midiDeviceInfo); + report.append("\n"); + } + public String reportAllMicrophones() { StringBuffer report = new StringBuffer(); report.append("\n############################"); diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/DynamicWorkloadActivity.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/DynamicWorkloadActivity.java index 917469aa1..8c94a3b7c 100644 --- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/DynamicWorkloadActivity.java +++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/DynamicWorkloadActivity.java @@ -34,13 +34,23 @@ /** * Demonstrate the behavior of a changing CPU load on underruns. * Display the workload and the callback duration in a chart. - * Enable PerformanceHints (ADPF). + * Enable or disable PerformanceHints (ADPF) using a checkbox. + * This might boost the CPU frequency when Oboe is taking too long to compute the next buffer. + * ADPF docs at: https://developer.android.com/reference/android/os/PerformanceHintManager */ public class DynamicWorkloadActivity extends TestOutputActivityBase { - private static final double WORKLOAD_MAX = 500.0; + private static final int WORKLOAD_HIGH_MIN = 30; + private static final int WORKLOAD_HIGH_MAX = 150; + // When the CPU is completely saturated then the load will be above 1.0. public static final double LOAD_RECOVERY_HIGH = 1.0; + // Use a slightly lower value for going low so that the comparator has hysteresis. public static final double LOAD_RECOVERY_LOW = 0.95; + private static final float MARGIN_ABOVE_WORKLOAD_FOR_CPU = 1.2f; + + // By default, set high workload to 70 voices, which is reasonable for most devices. + public static final double WORKLOAD_PROGRESS_FOR_70_VOICES = 0.53; + private Button mStopButton; private Button mStartButton; private TextView mResultView; @@ -57,34 +67,25 @@ public class DynamicWorkloadActivity extends TestOutputActivityBase { private CheckBox mDrawAlwaysBox; private int mCpuCount; + private static final int WORKLOAD_LOW = 1; + private int mWorkloadHigh; // this will get set later + private WorkloadView mDynamicWorkloadView; + // Periodically query the status of the streams. protected class WorkloadUpdateThread { public static final int SNIFFER_UPDATE_PERIOD_MSEC = 40; public static final int SNIFFER_UPDATE_DELAY_MSEC = 300; public static final int SNIFFER_TOGGLE_PERIOD_MSEC = 3000; - public static final int REQUIRED_STABLE_MEASUREMENTS = 20; - - private static final double WORKLOAD_FILTER_COEFFICIENT = 0.9; private static final int STATE_IDLE = 0; - private static final int STATE_BENCHMARK_TARGET = 1; - private static final int STATE_RUN_LOW = 2; - private static final int STATE_RUN_HIGH = 3; + private static final int STATE_RUN_LOW = 1; + private static final int STATE_RUN_HIGH = 2; private Handler mHandler; - private int mCount; - - private double mCpuLoadBenchmark = 0.90; // Determine workload that will hit this CPU load. - private double mCpuLoadHigh = 0.80; // Target CPU load during HIGH cycle. - private double mWorkloadLow = 0.0; - private double mWorkloadHigh = 0.0; - private double mWorkloadCurrent = 1.0; - private double mWorkloadBenchmark = 0.0; + private int mWorkloadCurrent = 1; private int mState = STATE_IDLE; private long mLastToggleTime = 0; - private int mStableCount = 0; - private boolean mArmLoadMonitor = false; private long mRecoveryTimeBegin; private long mRecoveryTimeEnd; private long mStartTimeNanos; @@ -93,8 +94,6 @@ String stateToString(int state) { switch(state) { case STATE_IDLE: return "Idle"; - case STATE_BENCHMARK_TARGET: - return "Benchmarking"; case STATE_RUN_LOW: return "low"; case STATE_RUN_HIGH: @@ -108,7 +107,7 @@ String stateToString(int state) { private Runnable runnableCode = new Runnable() { @Override public void run() { - double nextWorkload = 0.0; + int nextWorkload = mWorkloadCurrent; AudioStreamBase stream = mAudioOutTester.getCurrentAudioStream(); float cpuLoad = stream.getCpuLoad(); float maxCpuLoad = stream.getAndResetMaxCpuLoad(); @@ -119,29 +118,11 @@ public void run() { switch (mState) { case STATE_IDLE: drawChartOnce = true; // clear old chart - mState = STATE_BENCHMARK_TARGET; - break; - case STATE_BENCHMARK_TARGET: - // prevent divide by zero - double targetWorkload = (mWorkloadCurrent / Math.max(cpuLoad, 0.01)) * mCpuLoadBenchmark; - targetWorkload = Math.min(WORKLOAD_MAX, targetWorkload); - // low pass filter to find matching workload - nextWorkload = (WORKLOAD_FILTER_COEFFICIENT * mWorkloadCurrent) - + ((1.0 - WORKLOAD_FILTER_COEFFICIENT) * targetWorkload); - if (Math.abs(cpuLoad - mCpuLoadBenchmark) < 0.04) { - if (++mStableCount > REQUIRED_STABLE_MEASUREMENTS) { - // Found the right workload. - mWorkloadBenchmark = nextWorkload; - mLastToggleTime = now; - mState = STATE_RUN_LOW; - mWorkloadLow = Math.max(1, (int)(nextWorkload * 0.02)); - mWorkloadHigh = (int)(nextWorkload * (mCpuLoadHigh / mCpuLoadBenchmark)); - mWorkloadTrace.setMax((float)(2.0 * nextWorkload)); - } - } + mState = STATE_RUN_LOW; + mLastToggleTime = now; break; case STATE_RUN_LOW: - nextWorkload = mWorkloadLow; + nextWorkload = WORKLOAD_LOW; if ((now - mLastToggleTime) > SNIFFER_TOGGLE_PERIOD_MSEC) { mLastToggleTime = now; mState = STATE_RUN_HIGH; @@ -171,6 +152,8 @@ public void run() { } break; } + stream.setWorkload((int) nextWorkload); + mWorkloadCurrent = nextWorkload; // Update chart float nowMicros = (System.nanoTime() - mStartTimeNanos) * 0.001f; mMultiLineChart.addX(nowMicros); @@ -184,15 +167,12 @@ public void run() { String recoveryTimeString = (mRecoveryTimeEnd <= mRecoveryTimeBegin) ? "---" : ((mRecoveryTimeEnd - mRecoveryTimeBegin) + " msec"); String message = - "#Voices: max = " + String.format(Locale.getDefault(), "%d", (int) mWorkloadBenchmark) - + ", now = " + String.format(Locale.getDefault(), "%d", (int) nextWorkload) + "#Voices = " + (int) nextWorkload + "\nWorkState = " + stateToString(mState) + "\nCPU = " + String.format(Locale.getDefault(), "%6.3f%c", cpuLoad * 100, '%') + "\ncores = " + cpuMaskToString(cpuMask, mCpuCount) + "\nRecovery = " + recoveryTimeString; postResult(message); - stream.setWorkload((int)(nextWorkload)); - mWorkloadCurrent = nextWorkload; mHandler.postDelayed(runnableCode, SNIFFER_UPDATE_PERIOD_MSEC); } @@ -202,9 +182,7 @@ private void start() { stop(); mStartTimeNanos = System.nanoTime(); mMultiLineChart.reset(); - mCount = 0; - mStableCount = 0; - mState = STATE_BENCHMARK_TARGET; + mState = STATE_IDLE; mHandler = new Handler(Looper.getMainLooper()); // Start the initial runnable task by posting through the handler mHandler.postDelayed(runnableCode, SNIFFER_UPDATE_DELAY_MSEC); @@ -218,6 +196,10 @@ private void stop() { } + private void setWorkloadHigh(int workloadHigh) { + mWorkloadHigh = workloadHigh; + } + /** * This text will look best in a monospace font. @@ -259,9 +241,12 @@ protected void onCreate(Bundle savedInstanceState) { mStartButton = (Button) findViewById(R.id.button_start); mStopButton = (Button) findViewById(R.id.button_stop); + mDynamicWorkloadView = (WorkloadView) findViewById(R.id.dynamic_workload_view); + mWorkloadView.setVisibility(View.GONE); + // Add a row of checkboxes for setting CPU affinity. mCpuCount = NativeEngine.getCpuCount(); - final int defaultCpuAffinity = 2; + final int defaultCpuAffinityMask = 0; View.OnClickListener checkBoxListener = new View.OnClickListener() { @Override public void onClick(View view) { @@ -283,25 +268,26 @@ public void onClick(View view) { mAffinityBoxes.add(checkBox); checkBox.setText(cpuIndex + ""); checkBox.setOnClickListener(checkBoxListener); - if (cpuIndex == defaultCpuAffinity) { + if (((1 << cpuIndex) & defaultCpuAffinityMask) != 0) { checkBox.setChecked(true); } } - NativeEngine.setCpuAffinityMask(1 << defaultCpuAffinity); + NativeEngine.setCpuAffinityMask(defaultCpuAffinityMask); mMultiLineChart = (MultiLineChart) findViewById(R.id.multiline_chart); mMaxCpuLoadTrace = mMultiLineChart.createTrace("CPU", Color.RED, 0.0f, 2.0f); mWorkloadTrace = mMultiLineChart.createTrace("Work", Color.BLUE, - 0.0f, (float)WORKLOAD_MAX); + 0.0f, (MARGIN_ABOVE_WORKLOAD_FOR_CPU * WORKLOAD_HIGH_MAX)); - // TODO remove when finished with ADPF experiments. - mUseAltAdpfBox = (CheckBox) findViewById(R.id.use_alternative_adpf); mPerfHintBox = (CheckBox) findViewById(R.id.enable_perf_hint); + // TODO remove when finished with ADPF experiments. + mUseAltAdpfBox = (CheckBox) findViewById(R.id.use_alternative_adpf); mUseAltAdpfBox.setOnClickListener(buttonView -> { CheckBox checkBox = (CheckBox) buttonView; setUseAlternativeAdpf(checkBox.isChecked()); + mPerfHintBox.setEnabled(!checkBox.isChecked()); }); mUseAltAdpfBox.setVisibility(View.GONE); @@ -323,6 +309,16 @@ public void onClick(View view) { mDrawChartAlways = checkBox.isChecked(); }); + if (mDynamicWorkloadView != null) { + mDynamicWorkloadView.setWorkloadReceiver((w) -> { + setWorkloadHigh(w); + }); + + mDynamicWorkloadView.setLabel("High Workload"); + mDynamicWorkloadView.setRange(WORKLOAD_HIGH_MIN, WORKLOAD_HIGH_MAX); + mDynamicWorkloadView.setFaderNormalizedProgress(WORKLOAD_PROGRESS_FOR_70_VOICES); + } + updateButtons(false); updateEnabledWidgets(); hideSettingsViews(); // make more room @@ -356,9 +352,6 @@ int getActivityType() { } public void startTest(View view) { - // Do not draw until the benchmark stage has finished. - mDrawAlwaysBox.setChecked(false); - mDrawChartAlways = false; try { openAudio(); } catch (IOException e) { diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/GlitchActivity.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/GlitchActivity.java index 56ccec086..63abe5cf4 100644 --- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/GlitchActivity.java +++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/GlitchActivity.java @@ -47,9 +47,14 @@ public class GlitchActivity extends AnalyzerActivity { native int getStateFrameCount(int state); native int getGlitchCount(); + + // Number of frames in last glitch. + native int getGlitchLength(); + native double getPhase(); native double getSignalToNoiseDB(); native double getPeakAmplitude(); native double getSineAmplitude(); + native int getSinePeriod(); protected NativeSniffer mNativeSniffer = createNativeSniffer(); @@ -182,6 +187,7 @@ private String getCurrentStatusReport() { message.append(String.format(Locale.getDefault(), "time.no.glitches = %4.2f\n", mSecondsWithoutGlitches)); message.append(String.format(Locale.getDefault(), "max.time.no.glitches = %4.2f\n", mMaxSecondsWithoutGlitches)); + message.append(String.format(Locale.getDefault(), "glitch.length = %d\n", getGlitchLength())); message.append(String.format(Locale.getDefault(), "glitch.count = %d\n", mLastGlitchCount)); } return message.toString(); @@ -206,6 +212,7 @@ public void updateStatusText() { gatherData(); mLastGlitchReport = getCurrentStatusReport(); setAnalyzerText(mLastGlitchReport); + maybeDisplayWaveform(); } public double getMaxSecondsWithNoGlitch() { @@ -227,6 +234,8 @@ public void giveAdvice(String s) { protected void onGlitchDetected() { } + protected void maybeDisplayWaveform() {} + protected void setAnalyzerText(String s) { mAnalyzerTextView.setText(s); } @@ -257,6 +266,12 @@ public int getOutputChannel() { return mOutputChannel; } + /** + * Set the duration of a periodic forced glitch. + * @param frames or zero for no glitch + */ + public native void setForcedGlitchDuration(int frames); + public native void setInputChannelNative(int channel); public native void setOutputChannelNative(int channel); diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/ManualGlitchActivity.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/ManualGlitchActivity.java index 758ce0045..0ab0cc75c 100644 --- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/ManualGlitchActivity.java +++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/ManualGlitchActivity.java @@ -19,6 +19,9 @@ import android.os.Bundle; import android.os.Handler; import android.os.Looper; +import android.util.Log; +import android.view.View; +import android.widget.CheckBox; import android.widget.SeekBar; import android.widget.TextView; @@ -31,15 +34,19 @@ public class ManualGlitchActivity extends GlitchActivity { public static final int VALUE_DEFAULT_BUFFER_BURSTS = 2; public static final String KEY_TOLERANCE = "tolerance"; - private static final float DEFAULT_TOLERANCE = 0.1f; + private static final float DEFAULT_TOLERANCE = 0.10f; private static final long MIN_DISPLAY_PERIOD_MILLIS = 500; + private static final int WAVEFORM_SIZE = 400; private TextView mTextTolerance; private SeekBar mFaderTolerance; protected ExponentialTaper mTaperTolerance; + + private CheckBox mForceGlitchesBox; + private CheckBox mAutoScopeBox; private WaveformView mWaveformView; - private float[] mWaveform = new float[256]; + private float[] mWaveform = new float[WAVEFORM_SIZE]; private long mLastDisplayTime; private float mTolerance = DEFAULT_TOLERANCE; @@ -76,6 +83,8 @@ protected void onCreate(Bundle savedInstanceState) { mFaderTolerance.setOnSeekBarChangeListener(mToleranceListener); setToleranceFader(DEFAULT_TOLERANCE); + mForceGlitchesBox = (CheckBox) findViewById(R.id.boxForceGlitch); + mAutoScopeBox = (CheckBox) findViewById(R.id.boxAutoScope); mWaveformView = (WaveformView) findViewById(R.id.waveview_audio); } @@ -158,6 +167,7 @@ void stopAutomaticTest() { // Only call from UI thread. @Override public void onTestBegan() { + mAutoScopeBox.setChecked(true); mWaveformView.clearSampleData(); mWaveformView.postInvalidate(); super.onTestBegan(); @@ -166,19 +176,45 @@ public void onTestBegan() { // Called on UI thread @Override protected void onGlitchDetected() { + if (mAutoScopeBox.isChecked()) { + mAutoScopeBox.setChecked(false); // stop auto drawing of waveform + mLastDisplayTime = 0; // force draw first glitch + } long now = System.currentTimeMillis(); + Log.i(TAG,"onGlitchDetected: glitch"); if ((now - mLastDisplayTime) > MIN_DISPLAY_PERIOD_MILLIS) { mLastDisplayTime = now; int numSamples = getGlitch(mWaveform); mWaveformView.setSampleData(mWaveform, 0, numSamples); + int glitchLength = getGlitchLength(); + int[] cursors = new int[glitchLength > 0 ? 2 : 1]; + int startOfGlitch = getSinePeriod(); + cursors[0] = startOfGlitch; + if (glitchLength > 0) { + cursors[1] = startOfGlitch + getGlitchLength(); + } + mWaveformView.setCursorData(cursors); + Log.i(TAG,"onGlitchDetected: glitch, numSamples = " + numSamples); mWaveformView.postInvalidate(); } } - - private float[] getGlitchWaveform() { - return mWaveform; + @Override + protected void maybeDisplayWaveform() { + if (!mAutoScopeBox.isChecked()) return; + long now = System.currentTimeMillis(); + if ((now - mLastDisplayTime) > MIN_DISPLAY_PERIOD_MILLIS) { + mLastDisplayTime = now; + int numSamples = getRecentSamples(mWaveform); + mWaveformView.setSampleData(mWaveform, 0, numSamples); + mWaveformView.setCursorData(null); + mWaveformView.postInvalidate(); + } } private native int getGlitch(float[] mWaveform); + private native int getRecentSamples(float[] mWaveform); + public void onForceGlitchClicked(View view) { + setForcedGlitchDuration(mForceGlitchesBox.isChecked() ? 100 : 0); + } } diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestDataPathsActivity.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestDataPathsActivity.java index cfcb03649..49e66f4c7 100644 --- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestDataPathsActivity.java +++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestDataPathsActivity.java @@ -62,6 +62,9 @@ public class TestDataPathsActivity extends BaseAutoGlitchActivity { public static final String KEY_USE_ALL_OUTPUT_CHANNEL_MASKS = "use_all_output_channel_masks"; public static final boolean VALUE_DEFAULT_USE_ALL_OUTPUT_CHANNEL_MASKS = false; + public static final String KEY_USE_ALL_SAMPLE_RATES = "use_all_sample_rates"; + public static final boolean VALUE_DEFAULT_USE_ALL_SAMPLE_RATES = false; + public static final String KEY_SINGLE_TEST_INDEX = "single_test_index"; public static final int VALUE_DEFAULT_SINGLE_TEST_INDEX = -1; @@ -124,6 +127,7 @@ public class TestDataPathsActivity extends BaseAutoGlitchActivity { private CheckBox mCheckBoxInputDevices; private CheckBox mCheckBoxOutputDevices; private CheckBox mCheckBoxAllOutputChannelMasks; + private CheckBox mCheckBoxAllSampleRates; private static final int[] INPUT_PRESETS = { StreamConfiguration.INPUT_PRESET_GENERIC, @@ -166,6 +170,21 @@ public class TestDataPathsActivity extends BaseAutoGlitchActivity { StreamConfiguration.CHANNEL_7POINT1POINT4, }; + private static final int[] SAMPLE_RATES = { + 8000, + 11025, + 12000, + 16000, + 22050, + 24000, + 32000, + 44100, + 48000, + 64000, + 88200, + 96000, + }; + @NonNull public static String comparePassedField(String prefix, Object failed, Object passed, String name) { try { @@ -274,7 +293,6 @@ public String getShortReport() { native double getMagnitude(); native double getMaxMagnitude(); - native double getPhase(); @Override protected void inflateActivity() { @@ -290,6 +308,8 @@ protected void onCreate(Bundle savedInstanceState) { mCheckBoxOutputDevices = (CheckBox)findViewById(R.id.checkbox_paths_output_devices); mCheckBoxAllOutputChannelMasks = (CheckBox)findViewById(R.id.checkbox_paths_all_output_channel_masks); + mCheckBoxAllSampleRates = + (CheckBox)findViewById(R.id.checkbox_paths_all_sample_rates); } @Override @@ -393,18 +413,22 @@ String getOneLineSummary() { + ", IN" + (actualInConfig.isMMap() ? "-M" : "-L") + " D=" + actualInConfig.getDeviceId() + ", ch=" + channelText(getInputChannel(), actualInConfig.getChannelCount()) + + ", SR=" + actualInConfig.getSampleRate() + ", OUT" + (actualOutConfig.isMMap() ? "-M" : "-L") + " D=" + actualOutConfig.getDeviceId() + ", ch=" + channelText(getOutputChannel(), actualOutConfig.getChannelCount()) + + ", SR=" + actualOutConfig.getSampleRate() + ", mag = " + getMagnitudeText(mMaxMagnitude); } void setupDeviceCombo(int inputChannelCount, int inputChannelMask, int inputChannel, + int inputSampleRate, int outputChannelCount, int outputChannelMask, - int outputChannel) throws InterruptedException { + int outputChannel, + int outputSampleRate) throws InterruptedException { // Configure settings StreamConfiguration requestedInConfig = mAudioInputTester.requestedConfiguration; StreamConfiguration requestedOutConfig = mAudioOutTester.requestedConfiguration; @@ -418,6 +442,9 @@ void setupDeviceCombo(int inputChannelCount, requestedInConfig.setSharingMode(StreamConfiguration.SHARING_MODE_SHARED); requestedOutConfig.setSharingMode(StreamConfiguration.SHARING_MODE_SHARED); + requestedInConfig.setSampleRate(inputSampleRate); + requestedOutConfig.setSampleRate(outputSampleRate); + if (inputChannelMask != 0) { requestedInConfig.setChannelMask(inputChannelMask); } else { @@ -452,8 +479,8 @@ void testPresetCombo(int inputPreset, int outputChannel, boolean mmapEnabled ) throws InterruptedException { - setupDeviceCombo(numInputChannels, 0, inputChannel, numOutputChannels, 0, - outputChannel); + setupDeviceCombo(numInputChannels, 0, inputChannel, 48000, + numOutputChannels, 0, outputChannel, 48000); StreamConfiguration requestedInConfig = mAudioInputTester.requestedConfiguration; requestedInConfig.setInputPreset(inputPreset); @@ -508,20 +535,21 @@ void testInputDeviceCombo(int deviceId, int channelCount, int channelMask, int inputChannel, + int inputSampleRate, boolean mmapEnabled) throws InterruptedException { String typeString = AudioDeviceInfoConverter.typeToString(deviceType); if (channelMask != 0) { setTestName("Test InDev: #" + deviceId + " " + typeString + "_" + convertChannelMaskToText(channelMask) + "_" + - inputChannel + "/" + channelCount); + inputChannel + "/" + channelCount + "_" + inputSampleRate); } else { setTestName("Test InDev: #" + deviceId + " " + typeString - + "_" + inputChannel + "/" + channelCount); + + "_" + inputChannel + "/" + channelCount + "_" + inputSampleRate); } final int numOutputChannels = 2; - setupDeviceCombo(channelCount, channelMask, inputChannel, numOutputChannels, 0, - 0); + setupDeviceCombo(channelCount, channelMask, inputChannel, inputSampleRate, + numOutputChannels, 0, 0, 48000); StreamConfiguration requestedInConfig = mAudioInputTester.requestedConfiguration; requestedInConfig.setInputPreset(StreamConfiguration.INPUT_PRESET_VOICE_RECOGNITION); @@ -539,13 +567,14 @@ void testInputDeviceCombo(int deviceId, int deviceType, int channelCount, int channelMask, - int inputChannel) throws InterruptedException { + int inputChannel, + int inputSampleRate) throws InterruptedException { if (NativeEngine.isMMapSupported()) { testInputDeviceCombo(deviceId, deviceType, channelCount, channelMask, inputChannel, - true); + inputSampleRate, true); } testInputDeviceCombo(deviceId, deviceType, channelCount, channelMask, inputChannel, - false); + inputSampleRate, false); } void testInputDevices() throws InterruptedException { @@ -563,21 +592,29 @@ void testInputDevices() throws InterruptedException { int[] channelCounts = deviceInfo.getChannelCounts(); numTested++; // Always test mono and stereo. - testInputDeviceCombo(id, deviceType, 1, 0, 0); - testInputDeviceCombo(id, deviceType, 2, 0, 0); - testInputDeviceCombo(id, deviceType, 2, 0, 1); + testInputDeviceCombo(id, deviceType, 1, 0, 0, 48000); + testInputDeviceCombo(id, deviceType, 2, 0, 0, 48000); + testInputDeviceCombo(id, deviceType, 2, 0, 1, 48000); if (channelCounts.length > 0) { for (int numChannels : channelCounts) { // Test higher channel counts. if (numChannels > 2) { log("numChannels = " + numChannels + "\n"); for (int channel = 0; channel < numChannels; channel++) { - testInputDeviceCombo(id, deviceType, numChannels, 0, channel); + testInputDeviceCombo(id, deviceType, numChannels, 0, channel, + 48000); } } } } + runOnUiThread(() -> mCheckBoxAllSampleRates.setEnabled(false)); + if (mCheckBoxAllSampleRates.isChecked()) { + for (int sampleRate : SAMPLE_RATES) { + testInputDeviceCombo(id, deviceType, 1, 0, 0, sampleRate); + } + } + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.S_V2) { int[] channelMasks = deviceInfo.getChannelMasks(); if (channelMasks.length > 0) { @@ -592,7 +629,7 @@ void testInputDevices() throws InterruptedException { int channelCount = Integer.bitCount(nativeChannelMask); for (int channel = 0; channel < channelCount; channel++) { testInputDeviceCombo(id, deviceType, channelCount, nativeChannelMask, - channel); + channel, 48000); } } } @@ -661,19 +698,20 @@ void testOutputDeviceCombo(int deviceId, int channelCount, int channelMask, int outputChannel, + int outputSampleRate, boolean mmapEnabled) throws InterruptedException { String typeString = AudioDeviceInfoConverter.typeToString(deviceType); if (channelMask != 0) { setTestName("Test OutDev: #" + deviceId + " " + typeString - + " Mask:" + channelMask + "_" + outputChannel + "/" + channelCount); + + " Mask:" + channelMask + "_" + outputChannel + "/" + channelCount + "_" + outputSampleRate); } else { setTestName("Test InDev: #" + deviceId + " " + typeString - + "_" + outputChannel + "/" + channelCount); + + "_" + outputChannel + "/" + channelCount + "_" + outputSampleRate); } final int numInputChannels = 2; // TODO review, done because of mono problems on some devices - setupDeviceCombo(numInputChannels, 0, 0, channelCount, channelMask, - outputChannel); + setupDeviceCombo(numInputChannels, 0, 0, 48000, + channelCount, channelMask, outputChannel, outputSampleRate); StreamConfiguration requestedOutConfig = mAudioOutTester.requestedConfiguration; requestedOutConfig.setDeviceId(deviceId); @@ -703,13 +741,14 @@ void testOutputDeviceCombo(int deviceId, int deviceType, int channelCount, int channelMask, - int outputChannel) throws InterruptedException { + int outputChannel, + int outputSampleRate) throws InterruptedException { if (NativeEngine.isMMapSupported()) { testOutputDeviceCombo(deviceId, deviceType, channelCount, channelMask, outputChannel, - true); + outputSampleRate, true); } - testOutputDeviceCombo(deviceId, deviceType, channelCount, channelMask, outputChannel - , false); + testOutputDeviceCombo(deviceId, deviceType, channelCount, channelMask, outputChannel, + outputSampleRate, false); } void logBoth(String text) { @@ -738,21 +777,29 @@ void testOutputDevices() throws InterruptedException { int[] channelCounts = deviceInfo.getChannelCounts(); numTested++; // Always test mono and stereo. - testOutputDeviceCombo(id, deviceType, 1, 0, 0); - testOutputDeviceCombo(id, deviceType, 2, 0, 0); - testOutputDeviceCombo(id, deviceType, 2, 0, 1); + testOutputDeviceCombo(id, deviceType, 1, 0, 0, 48000); + testOutputDeviceCombo(id, deviceType, 2, 0, 0, 48000); + testOutputDeviceCombo(id, deviceType, 2, 0, 1, 48000); if (channelCounts.length > 0) { for (int numChannels : channelCounts) { // Test higher channel counts. if (numChannels > 2) { log("numChannels = " + numChannels + "\n"); for (int channel = 0; channel < numChannels; channel++) { - testOutputDeviceCombo(id, deviceType, numChannels, 0, channel); + testOutputDeviceCombo(id, deviceType, numChannels, 0, channel, + 48000); } } } } + runOnUiThread(() -> mCheckBoxAllSampleRates.setEnabled(false)); + if (mCheckBoxAllSampleRates.isChecked()) { + for (int sampleRate : SAMPLE_RATES) { + testOutputDeviceCombo(id, deviceType, 1, 0, 0, sampleRate); + } + } + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.S_V2 && deviceType == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER) { runOnUiThread(() -> mCheckBoxAllOutputChannelMasks.setEnabled(false)); @@ -762,7 +809,8 @@ void testOutputDevices() throws InterruptedException { log("channelMask = " + convertChannelMaskToText(channelMask) + "\n"); int channelCount = Integer.bitCount(channelMask); for (int channel = 0; channel < channelCount; channel++) { - testOutputDeviceCombo(id, deviceType, channelCount, channelMask, channel); + testOutputDeviceCombo(id, deviceType, channelCount, channelMask, + channel, 48000); } } } @@ -814,6 +862,7 @@ public void runTest() { mCheckBoxInputDevices.setEnabled(true); mCheckBoxOutputDevices.setEnabled(true); mCheckBoxAllOutputChannelMasks.setEnabled(true); + mCheckBoxAllSampleRates.setEnabled(true); keepScreenOn(false); }); } @@ -834,6 +883,9 @@ public void startTestUsingBundle() { boolean shouldUseAllOutputChannelMasks = mBundleFromIntent.getBoolean(KEY_USE_ALL_OUTPUT_CHANNEL_MASKS, VALUE_DEFAULT_USE_ALL_OUTPUT_CHANNEL_MASKS); + boolean shouldUseAllSampleRates = + mBundleFromIntent.getBoolean(KEY_USE_ALL_SAMPLE_RATES, + VALUE_DEFAULT_USE_ALL_SAMPLE_RATES); int singleTestIndex = mBundleFromIntent.getInt(KEY_SINGLE_TEST_INDEX, VALUE_DEFAULT_SINGLE_TEST_INDEX); @@ -842,6 +894,7 @@ public void startTestUsingBundle() { mCheckBoxInputDevices.setChecked(shouldUseInputDevices); mCheckBoxOutputDevices.setChecked(shouldUseOutputDevices); mCheckBoxAllOutputChannelMasks.setChecked(shouldUseAllOutputChannelMasks); + mCheckBoxAllSampleRates.setChecked(shouldUseAllSampleRates); mAutomatedTestRunner.setTestIndexText(singleTestIndex); }); diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestDisconnectActivity.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestDisconnectActivity.java index dc2a320f3..9e4d880d2 100644 --- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestDisconnectActivity.java +++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestDisconnectActivity.java @@ -20,6 +20,11 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.hardware.usb.UsbConstants; +import android.hardware.usb.UsbDevice; +import android.hardware.usb.UsbEndpoint; +import android.hardware.usb.UsbInterface; +import android.hardware.usb.UsbManager; import android.os.Bundle; import android.util.Log; import android.view.View; @@ -49,6 +54,7 @@ public class TestDisconnectActivity extends TestAudioActivity { private volatile boolean mTestFailed; private volatile boolean mSkipTest; private volatile int mPlugCount; + private volatile int mUsbDeviceAttachedCount; private volatile int mPlugState; private volatile int mPlugMicrophone; private BroadcastReceiver mPluginReceiver = new PluginBroadcastReceiver(); @@ -64,13 +70,34 @@ public class TestDisconnectActivity extends TestAudioActivity { public class PluginBroadcastReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { - mPlugMicrophone = intent.getIntExtra("microphone", -1); - mPlugState = intent.getIntExtra("state", -1); - mPlugCount++; + switch (intent.getAction()) { + case Intent.ACTION_HEADSET_PLUG: { + mPlugMicrophone = intent.getIntExtra("microphone", -1); + mPlugState = intent.getIntExtra("state", -1); + mPlugCount++; + } break; + case UsbManager.ACTION_USB_DEVICE_ATTACHED: + case UsbManager.ACTION_USB_DEVICE_DETACHED: { + UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); + final boolean hasAudioPlayback = + containsAudioStreamingInterface(device, UsbConstants.USB_DIR_OUT); + final boolean hasAudioCapture = + containsAudioStreamingInterface(device, UsbConstants.USB_DIR_IN); + if (hasAudioPlayback || hasAudioCapture) { + mPlugState = + intent.getAction() == UsbManager.ACTION_USB_DEVICE_ATTACHED ? 1 : 0; + mUsbDeviceAttachedCount++; + mPlugMicrophone = hasAudioCapture ? 1 : 0; + } + } break; + default: + break; + } runOnUiThread(new Runnable() { @Override public void run() { String message = "HEADSET_PLUG #" + mPlugCount + + ", USB_DEVICE_DE/ATTACHED #" + mUsbDeviceAttachedCount + ", mic = " + mPlugMicrophone + ", state = " + mPlugState; mPlugTextView.setText(message); @@ -78,6 +105,33 @@ public void run() { } }); } + + private static final int AUDIO_STREAMING_SUB_CLASS = 2; + + /** + * Figure out if an UsbDevice contains audio input/output streaming interface or not. + * + * @param device the given UsbDevice + * @param direction the direction of the audio streaming interface + * @return true if the UsbDevice contains the audio input/output streaming interface. + */ + private boolean containsAudioStreamingInterface(UsbDevice device, int direction) { + final int interfaceCount = device.getInterfaceCount(); + for (int i = 0; i < interfaceCount; ++i) { + UsbInterface usbInterface = device.getInterface(i); + if (usbInterface.getInterfaceClass() != UsbConstants.USB_CLASS_AUDIO + && usbInterface.getInterfaceSubclass() != AUDIO_STREAMING_SUB_CLASS) { + continue; + } + final int endpointCount = usbInterface.getEndpointCount(); + for (int j = 0; j < endpointCount; ++j) { + if (usbInterface.getEndpoint(j).getDirection() == direction) { + return true; + } + } + } + return false; + } } @Override @@ -152,6 +206,8 @@ public void run() { public void onResume() { super.onResume(); IntentFilter filter = new IntentFilter(Intent.ACTION_HEADSET_PLUG); + filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED); + filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED); this.registerReceiver(mPluginReceiver, filter); } @@ -238,11 +294,14 @@ private void testConfiguration(boolean isInput, return; } + updateFailSkipButton(true); + String actualConfigText = "none"; mSkipTest = false; + mTestFailed = false; // Try to synchronize with the current headset state, IN or OUT. - while (mAutomatedTestRunner.isThreadEnabled() && !mSkipTest) { + while (mAutomatedTestRunner.isThreadEnabled() && !mSkipTest && !mTestFailed) { if (requestPlugin != (mPlugState == 0)) { String message = "SYNC: " + (requestPlugin ? "UNplug" : "Plug IN") + " headset now!"; setInstructionsText(message); @@ -337,7 +396,6 @@ private void testConfiguration(boolean isInput, int oldPlugCount = mPlugCount; if (!openFailed && valid) { mTestFailed = false; - updateFailSkipButton(true); // poll until stream started while (!mTestFailed && mAutomatedTestRunner.isThreadEnabled() && !mSkipTest && stream.getState() == StreamConfiguration.STREAM_STATE_STARTING) { @@ -371,22 +429,25 @@ private void testConfiguration(boolean isInput, } } - if (mTestFailed) { - // Check whether the peripheral has a microphone. - // Sometimes the microphones does not appear on the first HEADSET_PLUG event. - if (isInput && (mPlugMicrophone == 0)) { - hasMicFailed = true; - } + if (mSkipTest) { + setStatusText("Skipped"); } else { - int error = stream.getLastErrorCallbackResult(); - if (error != StreamConfiguration.ERROR_DISCONNECTED) { - log("onErrorCallback error = " + error - + ", expected " + StreamConfiguration.ERROR_DISCONNECTED); - mTestFailed = true; + if (mTestFailed) { + // Check whether the peripheral has a microphone. + // Sometimes the microphones does not appear on the first HEADSET_PLUG event. + if (isInput && (mPlugMicrophone == 0)) { + hasMicFailed = true; + } + } else { + int error = stream.getLastErrorCallbackResult(); + if (error != StreamConfiguration.ERROR_DISCONNECTED) { + log("onErrorCallback error = " + error + + ", expected " + StreamConfiguration.ERROR_DISCONNECTED); + mTestFailed = true; + } } + setStatusText(mTestFailed ? "Failed" : "Passed - detected"); } - - setStatusText(mTestFailed ? "Failed" : "Passed - detected"); } updateFailSkipButton(false); setInstructionsText("Wait..."); diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestInputActivity.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestInputActivity.java index 5e2602b06..b4f2449c9 100644 --- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestInputActivity.java +++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestInputActivity.java @@ -73,7 +73,7 @@ protected void onCreate(Bundle savedInstanceState) { mWorkloadView = (WorkloadView) findViewById(R.id.workload_view); if (mWorkloadView != null) { - mWorkloadView.setAudioStreamTester(mAudioInputTester); + mWorkloadView.setWorkloadReceiver((w) -> mAudioInputTester.setWorkload(w)); } mCommunicationDeviceView = (CommunicationDeviceView) findViewById(R.id.comm_device_view); diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestOutputActivityBase.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestOutputActivityBase.java index 5c17e9283..731351b9c 100644 --- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestOutputActivityBase.java +++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestOutputActivityBase.java @@ -26,7 +26,7 @@ abstract class TestOutputActivityBase extends TestAudioActivity { AudioOutputTester mAudioOutTester; private BufferSizeView mBufferSizeView; - private WorkloadView mWorkloadView; + protected WorkloadView mWorkloadView; @Override boolean isOutput() { return true; } @@ -40,13 +40,14 @@ protected void findAudioCommon() { super.findAudioCommon(); mBufferSizeView = (BufferSizeView) findViewById(R.id.buffer_size_view); mWorkloadView = (WorkloadView) findViewById(R.id.workload_view); + if (mWorkloadView != null) { + mWorkloadView.setWorkloadReceiver((w) -> mAudioOutTester.setWorkload(w)); + } } @Override public AudioOutputTester addAudioOutputTester() { - AudioOutputTester audioOutTester = super.addAudioOutputTester(); - mWorkloadView.setAudioStreamTester(audioOutTester); - return audioOutTester; + return super.addAudioOutputTester(); } @Override diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/WaveformView.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/WaveformView.java index c073ce96d..4f4705c01 100644 --- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/WaveformView.java +++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/WaveformView.java @@ -106,7 +106,18 @@ protected void onDraw(Canvas canvas) { if (localData == null || mSampleCount == 0) { return; } + float xScale = ((float) mCurrentWidth) / (mSampleCount - 1); + + // Draw cursors. + if (mCursors != null) { + for (int i = 0; i < mCursors.length; i++) { + float x = mCursors[i] * xScale; + canvas.drawLine(x, 0, x, mCurrentHeight, mCursorPaint); + } + } + + // Draw waveform. float x0 = 0.0f; if (xScale < 1.0) { // Draw a vertical bar for multiple samples. @@ -136,12 +147,6 @@ protected void onDraw(Canvas canvas) { y0 = y1; } } - if (mCursors != null) { - for (int i = 0; i < mCursors.length; i++) { - float x = mCursors[i] * xScale; - canvas.drawLine(x, 0, x, mCurrentHeight, mCursorPaint); - } - } } /** diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/WorkloadView.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/WorkloadView.java index e1dc9d5e5..74027c2c0 100644 --- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/WorkloadView.java +++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/WorkloadView.java @@ -18,6 +18,7 @@ import android.content.Context; import android.util.AttributeSet; +import android.util.Log; import android.view.LayoutInflater; import android.widget.LinearLayout; import android.widget.SeekBar; @@ -27,13 +28,19 @@ public class WorkloadView extends LinearLayout { - private AudioStreamTester mAudioStreamTester; - protected static final int FADER_PROGRESS_MAX = 1000; // must match layout protected TextView mTextView; protected SeekBar mSeekBar; + + private String mLabel = "Workload"; protected ExponentialTaper mExponentialTaper; + public interface WorkloadReceiver { + void setWorkload(int workload); + } + + WorkloadReceiver mWorkloadReceiver; + private SeekBar.OnSeekBarChangeListener mChangeListener = new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { @@ -66,12 +73,8 @@ public WorkloadView(Context context, initializeViews(context); } - public AudioStreamTester getAudioStreamTester() { - return mAudioStreamTester; - } - - public void setAudioStreamTester(AudioStreamTester audioStreamTester) { - mAudioStreamTester = audioStreamTester; + public void setWorkloadReceiver(WorkloadReceiver workloadReceiver) { + mWorkloadReceiver = workloadReceiver; } void setFaderNormalizedProgress(double fraction) { @@ -92,15 +95,21 @@ private void initializeViews(Context context) { mTextView = (TextView) findViewById(R.id.textWorkload); mSeekBar = (SeekBar) findViewById(R.id.faderWorkload); mSeekBar.setOnSeekBarChangeListener(mChangeListener); - mExponentialTaper = new ExponentialTaper(0.0, 100.0, 10.0); + setRange(0.0, 100.0); //mSeekBar.setProgress(0); } + void setRange(double dMin, double dMax) { + mExponentialTaper = new ExponentialTaper(dMin, dMax, 10.0); + } + private void setValueByPosition(int progress) { int workload = (int) mExponentialTaper.linearToExponential( ((double)progress) / FADER_PROGRESS_MAX); - mAudioStreamTester.setWorkload(workload); - mTextView.setText("Workload = " + String.format(Locale.getDefault(), "%3d", workload)); + if (mWorkloadReceiver != null) { + mWorkloadReceiver.setWorkload(workload); + } + mTextView.setText(getLabel() + " = " + String.format(Locale.getDefault(), "%3d", workload)); } @Override @@ -108,4 +117,12 @@ public void setEnabled(boolean enabled) { super.setEnabled(enabled); mSeekBar.setEnabled(enabled); } + + public String getLabel() { + return mLabel; + } + + public void setLabel(String label) { + this.mLabel = label; + } } diff --git a/apps/OboeTester/app/src/main/res/layout/activity_data_paths.xml b/apps/OboeTester/app/src/main/res/layout/activity_data_paths.xml index 1803a2fbd..52d8da948 100644 --- a/apps/OboeTester/app/src/main/res/layout/activity_data_paths.xml +++ b/apps/OboeTester/app/src/main/res/layout/activity_data_paths.xml @@ -35,6 +35,12 @@ android:layout_height="wrap_content" android:checked="true" android:text="OutDev" /> + + + + + + android:text="ADPF" /> + + + + + + + + diff --git a/apps/OboeTester/app/src/main/res/layout/activity_test_disconnect.xml b/apps/OboeTester/app/src/main/res/layout/activity_test_disconnect.xml index c09bda05f..345e82047 100644 --- a/apps/OboeTester/app/src/main/res/layout/activity_test_disconnect.xml +++ b/apps/OboeTester/app/src/main/res/layout/activity_test_disconnect.xml @@ -24,7 +24,7 @@ android:id="@+id/text_plug_events" android:layout_width="match_parent" android:layout_height="wrap_content" - android:lines="1" + android:lines="2" android:text="plug #" android:textSize="18sp" android:textStyle="bold" diff --git a/apps/OboeTester/app/src/main/res/values/strings.xml b/apps/OboeTester/app/src/main/res/values/strings.xml index d3157b3a0..3f96c0de4 100644 --- a/apps/OboeTester/app/src/main/res/values/strings.xml +++ b/apps/OboeTester/app/src/main/res/values/strings.xml @@ -198,7 +198,11 @@ , Hang(ms) 0 + 1 + 2 + 3 4 + 5 10 20 30 diff --git a/apps/OboeTester/docs/AutomatedTesting.md b/apps/OboeTester/docs/AutomatedTesting.md index e404c76e1..f4532b8b8 100644 --- a/apps/OboeTester/docs/AutomatedTesting.md +++ b/apps/OboeTester/docs/AutomatedTesting.md @@ -114,6 +114,7 @@ There are several optional parameters for just the "data_paths" test: --ez use_input_devices {"true", 1, "false", 0} // Whether to test various input devices. Note use of "-ez" --ez use_output_devices {"true", 1, "false", 0} // Whether to test various output devices. Note use of "-ez" --ez use_all_output_channel_masks {"true", 1, "false", 0} // Whether to test all output channel masks. Note use of "-ez". Default is false + --ez use_all_sample_rates {"true", 1, "false", 0} // Whether to test all sample rates. Note use of "-ez". Default is false --ei single_test_index {testId} // Index for testing one specific test There are some optional parameters for just the "output" test: diff --git a/apps/OboeTester/docs/Usage.md b/apps/OboeTester/docs/Usage.md index 98a6266a8..6ff693898 100644 --- a/apps/OboeTester/docs/Usage.md +++ b/apps/OboeTester/docs/Usage.md @@ -166,3 +166,8 @@ Lets you delete an error callback while it is used by Oboe. Targeted test for is ### Route Callback Test Changes the VoiceCommunication route while playing audio. Targeted test for issue #1763. + +### CPU Load +This test plays a tone and alternates between low and high workloads. +It exercises the kernel's CPU scheduler, which controls CPU frequency and core migration. +Moredetails on the [wiki/OboeTester_DynamicCpuLoad](https://github.com/google/oboe/wiki/OboeTester_DynamicCpuLoad). diff --git a/include/oboe/AudioStream.h b/include/oboe/AudioStream.h index 261772a1a..01b8c9766 100644 --- a/include/oboe/AudioStream.h +++ b/include/oboe/AudioStream.h @@ -194,6 +194,13 @@ class AudioStream : public AudioStreamBase { * * This cannot be set higher than getBufferCapacity(). * + * This should only be used with Output streams. It will + * be ignored for Input streams because they are generally kept as empty as possible. + * + * For OpenSL ES, this method only has an effect on output stream that do NOT + * use a callback. The blocking writes goes into a buffer in Oboe and the size of that + * buffer is controlled by this method. + * * @param requestedFrames requested number of frames that can be filled without blocking * @return the resulting buffer size in frames (obtained using value()) or an error (obtained * using error()) diff --git a/include/oboe/AudioStreamBuilder.h b/include/oboe/AudioStreamBuilder.h index 89e1447ef..1574a3980 100644 --- a/include/oboe/AudioStreamBuilder.h +++ b/include/oboe/AudioStreamBuilder.h @@ -647,6 +647,14 @@ class AudioStreamBuilder : public AudioStreamBase { private: + /** + * Use this internally to implement opening with a shared_ptr. + * + * @param stream pointer to a variable to receive the stream address + * @return OBOE_OK if successful or a negative error code. + */ + Result openStreamInternal(AudioStream **streamPP); + /** * @param other * @return true if channels, format and sample rate match diff --git a/include/oboe/AudioStreamCallback.h b/include/oboe/AudioStreamCallback.h index 17d28ba70..8d8e2feb4 100644 --- a/include/oboe/AudioStreamCallback.h +++ b/include/oboe/AudioStreamCallback.h @@ -48,7 +48,8 @@ class AudioStreamDataCallback { * write() on the stream that is making the callback. * * Note that numFrames can vary unless AudioStreamBuilder::setFramesPerCallback() - * is called. + * is called. If AudioStreamBuilder::setFramesPerCallback() is NOT called then + * numFrames should always be <= AudioStream::getFramesPerBurst(). * * Also note that this callback function should be considered a "real-time" function. * It must not do anything that could cause an unbounded delay because that can cause the diff --git a/include/oboe/Version.h b/include/oboe/Version.h index 4c77a6754..96ef82bd8 100644 --- a/include/oboe/Version.h +++ b/include/oboe/Version.h @@ -37,7 +37,7 @@ #define OBOE_VERSION_MINOR 8 // Type: 16-bit unsigned int. Min value: 0 Max value: 65535. See below for description. -#define OBOE_VERSION_PATCH 0 +#define OBOE_VERSION_PATCH 1 #define OBOE_STRINGIFY(x) #x #define OBOE_TOSTRING(x) OBOE_STRINGIFY(x) diff --git a/src/common/AudioStreamBuilder.cpp b/src/common/AudioStreamBuilder.cpp index 5dbe38cc8..f655f9fc7 100644 --- a/src/common/AudioStreamBuilder.cpp +++ b/src/common/AudioStreamBuilder.cpp @@ -89,6 +89,11 @@ bool AudioStreamBuilder::isCompatible(AudioStreamBase &other) { } Result AudioStreamBuilder::openStream(AudioStream **streamPP) { + LOGW("Passing AudioStream pointer deprecated, Use openStream(std::shared_ptr &stream) instead."); + return openStreamInternal(streamPP); +} + +Result AudioStreamBuilder::openStreamInternal(AudioStream **streamPP) { auto result = isValidConfig(); if (result != Result::OK) { LOGW("%s() invalid config %d", __func__, result); @@ -202,6 +207,7 @@ Result AudioStreamBuilder::openStream(AudioStream **streamPP) { } Result AudioStreamBuilder::openManagedStream(oboe::ManagedStream &stream) { + LOGW("`openManagedStream` is deprecated. Use openStream(std::shared_ptr &stream) instead."); stream.reset(); AudioStream *streamptr; auto result = openStream(&streamptr); @@ -212,7 +218,7 @@ Result AudioStreamBuilder::openManagedStream(oboe::ManagedStream &stream) { Result AudioStreamBuilder::openStream(std::shared_ptr &sharedStream) { sharedStream.reset(); AudioStream *streamptr; - auto result = openStream(&streamptr); + auto result = openStreamInternal(&streamptr); if (result == Result::OK) { sharedStream.reset(streamptr); // Save a weak_ptr in the stream for use with callbacks. diff --git a/src/flowgraph/FlowgraphUtilities.h b/src/flowgraph/FlowgraphUtilities.h index 5e90588ce..e277d6e6f 100644 --- a/src/flowgraph/FlowgraphUtilities.h +++ b/src/flowgraph/FlowgraphUtilities.h @@ -17,6 +17,7 @@ #ifndef FLOWGRAPH_UTILITIES_H #define FLOWGRAPH_UTILITIES_H +#include #include using namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph; @@ -50,6 +51,20 @@ static int32_t clamp32FromFloat(float f) return f > 0 ? f + 0.5 : f - 0.5; } +/** + * Convert a single-precision floating point value to a Q0.23 integer value, stored in a + * 32 bit signed integer (technically stored as Q8.23, but clamped to Q0.23). + * + * Values outside the range [-1.0, 1.0) are properly clamped to -8388608 and 8388607, + * including -Inf and +Inf. NaN values are considered undefined, and behavior may change + * depending on hardware and future implementation of this function. + */ +static int32_t clamp24FromFloat(float f) +{ + static const float scale = 1 << 23; + return (int32_t) lroundf(fmaxf(fminf(f * scale, scale - 1.f), -scale)); +} + }; #endif // FLOWGRAPH_UTILITIES_H diff --git a/src/flowgraph/SinkI8_24.cpp b/src/flowgraph/SinkI8_24.cpp new file mode 100644 index 000000000..d5e4b808a --- /dev/null +++ b/src/flowgraph/SinkI8_24.cpp @@ -0,0 +1,55 @@ +/* + * Copyright 2023 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 "FlowGraphNode.h" +#include "FlowgraphUtilities.h" +#include "SinkI8_24.h" + +#if FLOWGRAPH_ANDROID_INTERNAL +#include +#endif + +using namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph; + +SinkI8_24::SinkI8_24(int32_t channelCount) + : FlowGraphSink(channelCount) {} + +int32_t SinkI8_24::read(void *data, int32_t numFrames) { + int32_t *intData = (int32_t *) data; + const int32_t channelCount = input.getSamplesPerFrame(); + + int32_t framesLeft = numFrames; + while (framesLeft > 0) { + // Run the graph and pull data through the input port. + int32_t framesRead = pullData(framesLeft); + if (framesRead <= 0) { + break; + } + const float *signal = input.getBuffer(); + int32_t numSamples = framesRead * channelCount; +#if FLOWGRAPH_ANDROID_INTERNAL + memcpy_to_q8_23_from_float_with_clamp(intData, signal, numSamples); + intData += numSamples; + signal += numSamples; +#else + for (int i = 0; i < numSamples; i++) { + *intData++ = FlowgraphUtilities::clamp24FromFloat(*signal++); + } +#endif + framesLeft -= framesRead; + } + return numFrames - framesLeft; +} diff --git a/src/flowgraph/SinkI8_24.h b/src/flowgraph/SinkI8_24.h new file mode 100644 index 000000000..366b4ba81 --- /dev/null +++ b/src/flowgraph/SinkI8_24.h @@ -0,0 +1,40 @@ +/* + * Copyright 2023 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 FLOWGRAPH_SINK_I8_24_H +#define FLOWGRAPH_SINK_I8_24_H + +#include + +#include "FlowGraphNode.h" + +namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph { + + class SinkI8_24 : public FlowGraphSink { + public: + explicit SinkI8_24(int32_t channelCount); + ~SinkI8_24() override = default; + + int32_t read(void *data, int32_t numFrames) override; + + const char *getName() override { + return "SinkI8_24"; + } + }; + +} /* namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph */ + +#endif //FLOWGRAPH_SINK_I8_24_H diff --git a/src/flowgraph/SourceI8_24.cpp b/src/flowgraph/SourceI8_24.cpp new file mode 100644 index 000000000..684446cc0 --- /dev/null +++ b/src/flowgraph/SourceI8_24.cpp @@ -0,0 +1,54 @@ +/* + * Copyright 2023 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 +#include + +#include "FlowGraphNode.h" +#include "SourceI8_24.h" + +#if FLOWGRAPH_ANDROID_INTERNAL +#include +#endif + +using namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph; + +SourceI8_24::SourceI8_24(int32_t channelCount) + : FlowGraphSourceBuffered(channelCount) { +} + +int32_t SourceI8_24::onProcess(int32_t numFrames) { + float *floatData = output.getBuffer(); + const int32_t channelCount = output.getSamplesPerFrame(); + + const int32_t framesLeft = mSizeInFrames - mFrameIndex; + const int32_t framesToProcess = std::min(numFrames, framesLeft); + const int32_t numSamples = framesToProcess * channelCount; + + const int32_t *intBase = static_cast(mData); + const int32_t *intData = &intBase[mFrameIndex * channelCount]; + +#if FLOWGRAPH_ANDROID_INTERNAL + memcpy_to_float_from_q8_23(floatData, intData, numSamples); +#else + for (int i = 0; i < numSamples; i++) { + *floatData++ = *intData++ * kScale; + } +#endif + + mFrameIndex += framesToProcess; + return framesToProcess; +} diff --git a/src/flowgraph/SourceI8_24.h b/src/flowgraph/SourceI8_24.h new file mode 100644 index 000000000..91c756c8d --- /dev/null +++ b/src/flowgraph/SourceI8_24.h @@ -0,0 +1,42 @@ +/* + * Copyright 2023 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 FLOWGRAPH_SOURCE_I8_24_H +#define FLOWGRAPH_SOURCE_I8_24_H + +#include + +#include "FlowGraphNode.h" + +namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph { + +class SourceI8_24 : public FlowGraphSourceBuffered { +public: + explicit SourceI8_24(int32_t channelCount); + ~SourceI8_24() override = default; + + int32_t onProcess(int32_t numFrames) override; + + const char *getName() override { + return "SourceI8_24"; + } +private: + static constexpr float kScale = 1.0 / (1UL << 23); +}; + +} /* namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph */ + +#endif //FLOWGRAPH_SOURCE_I8_24_H diff --git a/src/flowgraph/resampler/MultiChannelResampler.cpp b/src/flowgraph/resampler/MultiChannelResampler.cpp index 611ddcd15..245b669db 100644 --- a/src/flowgraph/resampler/MultiChannelResampler.cpp +++ b/src/flowgraph/resampler/MultiChannelResampler.cpp @@ -120,7 +120,7 @@ void MultiChannelResampler::writeFrame(const float *frame) { } float MultiChannelResampler::sinc(float radians) { - if (abs(radians) < 1.0e-9) return 1.0f; // avoid divide by zero + if (fabsf(radians) < 1.0e-9f) return 1.0f; // avoid divide by zero return sinf(radians) / radians; // Sinc function } diff --git a/src/opensles/AudioOutputStreamOpenSLES.cpp b/src/opensles/AudioOutputStreamOpenSLES.cpp index 91f9882d5..0ef87dd01 100644 --- a/src/opensles/AudioOutputStreamOpenSLES.cpp +++ b/src/opensles/AudioOutputStreamOpenSLES.cpp @@ -30,9 +30,10 @@ using namespace oboe; static SLuint32 OpenSLES_convertOutputUsage(Usage oboeUsage) { - SLuint32 openslStream = SL_ANDROID_STREAM_MEDIA; + SLuint32 openslStream; switch(oboeUsage) { case Usage::Media: + case Usage::Game: openslStream = SL_ANDROID_STREAM_MEDIA; break; case Usage::VoiceCommunication: @@ -43,18 +44,15 @@ static SLuint32 OpenSLES_convertOutputUsage(Usage oboeUsage) { openslStream = SL_ANDROID_STREAM_ALARM; break; case Usage::Notification: - case Usage::NotificationRingtone: case Usage::NotificationEvent: openslStream = SL_ANDROID_STREAM_NOTIFICATION; break; + case Usage::NotificationRingtone: + openslStream = SL_ANDROID_STREAM_RING; + break; case Usage::AssistanceAccessibility: case Usage::AssistanceNavigationGuidance: case Usage::AssistanceSonification: - openslStream = SL_ANDROID_STREAM_SYSTEM; - break; - case Usage::Game: - openslStream = SL_ANDROID_STREAM_MEDIA; - break; case Usage::Assistant: default: openslStream = SL_ANDROID_STREAM_SYSTEM;