aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorandroid-build-team Robot <android-build-team-robot@google.com>2021-03-04 02:04:18 +0000
committerandroid-build-team Robot <android-build-team-robot@google.com>2021-03-04 02:04:18 +0000
commita1428cdccbaf05b47945d12c3eb142f6d5131478 (patch)
tree0b5f9f0ab495f34097724df6c7c771e6264b2700
parentf55f846d1eb2aae816a21dfa3db47444d39a1ea8 (diff)
parent7592d8332c301ca0999a3f441e189cc899552382 (diff)
downloadoboe-a1428cdccbaf05b47945d12c3eb142f6d5131478.tar.gz
Snap for 7183400 from 7592d8332c301ca0999a3f441e189cc899552382 to sc-release
Change-Id: I8e222bba5cab819a38de1fe0f467af77c831ee84
-rwxr-xr-x.ci_tools/build_samples.sh46
-rw-r--r--.ci_tools/misc_ci.sh19
-rw-r--r--.ci_tools/run_samples.sh4
-rw-r--r--.ci_tools/setup_env.sh44
-rw-r--r--.github/ISSUE_TEMPLATE/bug_report.md2
-rw-r--r--.github/workflows/android.yml37
-rw-r--r--.travis.yml30
-rw-r--r--Android.bp5
-rw-r--r--METADATA8
-rw-r--r--README.md11
-rw-r--r--apps/OboeTester/app/CMakeLists.txt4
-rw-r--r--apps/OboeTester/app/build.gradle4
-rw-r--r--apps/OboeTester/app/src/main/AndroidManifest.xml14
-rw-r--r--apps/OboeTester/app/src/main/cpp/FullDuplexAnalyzer.h12
-rw-r--r--apps/OboeTester/app/src/main/cpp/FullDuplexGlitches.cpp18
-rw-r--r--apps/OboeTester/app/src/main/cpp/FullDuplexGlitches.h53
-rw-r--r--apps/OboeTester/app/src/main/cpp/FullDuplexLatency.cpp44
-rw-r--r--apps/OboeTester/app/src/main/cpp/FullDuplexLatency.h65
-rw-r--r--apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp85
-rw-r--r--apps/OboeTester/app/src/main/cpp/NativeAudioContext.h310
-rw-r--r--apps/OboeTester/app/src/main/cpp/analyzer/BaseSineAnalyzer.h203
-rw-r--r--apps/OboeTester/app/src/main/cpp/analyzer/DataPathAnalyzer.h94
-rw-r--r--apps/OboeTester/app/src/main/cpp/analyzer/GlitchAnalyzer.h132
-rw-r--r--apps/OboeTester/app/src/main/cpp/analyzer/LatencyAnalyzer.h4
-rw-r--r--apps/OboeTester/app/src/main/cpp/jni-bridge.cpp107
-rw-r--r--apps/OboeTester/app/src/main/java/com/google/sample/audio_device/AudioDeviceInfoConverter.java6
-rw-r--r--apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/AnalyzerActivity.java12
-rw-r--r--apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/AudioStreamBase.java36
-rw-r--r--apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/AutoGlitchActivity.java330
-rw-r--r--apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/AutomatedGlitchActivity.java114
-rw-r--r--apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/AutomatedTestRunner.java285
-rw-r--r--apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/BaseAutoGlitchActivity.java227
-rw-r--r--apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/BufferSizeView.java166
-rw-r--r--apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/CachedTextViewLog.java72
-rw-r--r--apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/EchoActivity.java83
-rw-r--r--apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/GlitchActivity.java167
-rw-r--r--apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/MainActivity.java68
-rw-r--r--apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/NativeSniffer.java69
-rw-r--r--apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/OboeAudioStream.java4
-rw-r--r--apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/RecorderActivity.java6
-rw-r--r--apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/RoundTripLatencyActivity.java96
-rw-r--r--apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/StreamConfiguration.java34
-rw-r--r--apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TapLatencyAnalyser.java10
-rw-r--r--apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TapToToneActivity.java15
-rw-r--r--apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestAudioActivity.java61
-rw-r--r--apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestDataPathsActivity.java512
-rw-r--r--apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestDisconnectActivity.java202
-rw-r--r--apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestInputActivity.java11
-rw-r--r--apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestOutputActivity.java5
-rw-r--r--apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestOutputActivityBase.java3
-rw-r--r--apps/OboeTester/app/src/main/res/layout/activity_auto_glitches.xml52
-rw-r--r--apps/OboeTester/app/src/main/res/layout/activity_data_paths.xml29
-rw-r--r--apps/OboeTester/app/src/main/res/layout/activity_device_report.xml3
-rw-r--r--apps/OboeTester/app/src/main/res/layout/activity_echo.xml10
-rw-r--r--apps/OboeTester/app/src/main/res/layout/activity_main.xml10
-rw-r--r--apps/OboeTester/app/src/main/res/layout/activity_manual_glitches.xml2
-rw-r--r--apps/OboeTester/app/src/main/res/layout/activity_rt_latency.xml15
-rw-r--r--apps/OboeTester/app/src/main/res/layout/activity_test_disconnect.xml52
-rw-r--r--apps/OboeTester/app/src/main/res/layout/auto_test_runner.xml76
-rw-r--r--apps/OboeTester/app/src/main/res/layout/buffer_size_view.xml35
-rw-r--r--apps/OboeTester/app/src/main/res/layout/stream_config.xml2
-rw-r--r--apps/OboeTester/app/src/main/res/values/strings.xml29
-rw-r--r--apps/OboeTester/docs/Usage.md16
-rw-r--r--apps/fxlab/app/src/main/java/com/mobileer/androidfxlab/MainActivity.kt23
-rw-r--r--apps/fxlab/build.gradle2
-rw-r--r--apps/fxlab/gradle/wrapper/gradle-wrapper.properties2
-rw-r--r--docs/AppsUsingOboe.md1
-rw-r--r--docs/FullGuide.md19
-rw-r--r--docs/GettingStarted.md98
-rw-r--r--include/oboe/AudioStreamBuilder.h5
-rw-r--r--include/oboe/Version.h2
-rw-r--r--samples/LiveEffect/README.md5
-rw-r--r--samples/LiveEffect/src/main/cpp/FullDuplexPass.h2
-rw-r--r--samples/drumthumper/README.md2
-rw-r--r--samples/drumthumper/src/main/res/layout-land/drumthumper_activity.xml8
-rw-r--r--samples/drumthumper/src/main/res/layout/drumthumper_activity.xml8
-rw-r--r--src/aaudio/AAudioExtensions.h172
-rw-r--r--src/aaudio/AAudioLoader.cpp23
-rw-r--r--src/aaudio/AAudioLoader.h11
-rw-r--r--src/aaudio/AudioStreamAAudio.cpp3
-rw-r--r--src/common/AudioStreamBuilder.cpp18
-rw-r--r--src/common/FilterAudioStream.h5
-rw-r--r--src/common/QuirksManager.cpp25
-rw-r--r--src/common/QuirksManager.h10
-rw-r--r--src/flowgraph/resampler/README.md12
-rw-r--r--tests/README.md4
-rw-r--r--tests/testStreamOpen.cpp5
87 files changed, 3118 insertions, 1627 deletions
diff --git a/.ci_tools/build_samples.sh b/.ci_tools/build_samples.sh
deleted file mode 100755
index ec3bb7c3..00000000
--- a/.ci_tools/build_samples.sh
+++ /dev/null
@@ -1,46 +0,0 @@
-# Configurations:
-# temp file name to hold build result
-BUILD_RESULT_FILE=build_result.txt
-
-# Repo root directory
-REPO_ROOT_DIR=.
-
-
-declare projects=(
- samples
- apps/OboeTester
-)
-
-for d in "${projects[@]}"; do
- pushd ${REPO_ROOT_DIR}/${d} >/dev/null
- TERM=dumb ./gradlew -q clean bundleDebug
- popd >/dev/null
-done
-
-
-# Check the apks that all get built fine (RhythmGame uses split APKs so we have to specify each one)
-declare bundles=(
- samples/hello-oboe/build/outputs/bundle/debug/hello-oboe-debug.aab
- samples/MegaDrone/build/outputs/bundle/debug/MegaDrone-debug.aab
- samples/RhythmGame/build/outputs/bundle/ndkExtractorDebug/RhythmGame-ndkExtractor-debug.aab
- samples/LiveEffect/build/outputs/bundle/debug/LiveEffect-debug.aab
- apps/OboeTester/app/build/outputs/bundle/debug/app-debug.aab
- samples/drumthumper/build/outputs/bundle/debug/drumthumper-debug.aab
-)
-
-rm -fr ${BUILD_RESULT_FILE}
-for bundle in "${bundles[@]}"; do
- if [ ! -f ${REPO_ROOT_DIR}/${bundle} ]; then
- export SAMPLE_CI_RESULT=1
- echo ${bundle} does not build >> ${BUILD_RESULT_FILE}
- fi
-done
-
-if [ -f ${BUILD_RESULT_FILE} ]; then
- echo "******* Failed Builds ********:"
- cat ${BUILD_RESULT_FILE}
-else
- echo "======= BUILD SUCCESS ======="
-fi
-
-rm -fr ${BUILD_RESULT_FILE}
diff --git a/.ci_tools/misc_ci.sh b/.ci_tools/misc_ci.sh
deleted file mode 100644
index 01bd2481..00000000
--- a/.ci_tools/misc_ci.sh
+++ /dev/null
@@ -1,19 +0,0 @@
-#!/bin/bash
-set +e
-
-MISC_STATUS=0
-
-# check that all targetSdkVersion are 26+
-# test "$(grep -H targetSdkVersion */app/build.gradle | tee /dev/stderr | cut -d= -f 2 | xargs -n1 echo | sort | uniq | wc -l)" = "2"
-# check that there is no tabs in AndroidManifest
-(! grep -n $'\t' */*/src/main/AndroidManifest.xml) | cat -t;
-MISC_STATUS=$(($MISC_STATUS + ${PIPESTATUS[0]}))
-
-# check that there is no trailing spaces in AndroidManifest
-(! grep -E '\s+$' */*/src/main/AndroidManifest.xml) | cat -e;
-MISC_STATUS=$(($MISC_STATUS + ${PIPESTATUS[0]}))
-
-# populate the error to final status
-if [[ "$MISC_STATUS" -ne 0 ]]; then
- SAMPLE_CI_RESULT=$(($SAMPLE_CI_RESULT + 1))
-fi
diff --git a/.ci_tools/run_samples.sh b/.ci_tools/run_samples.sh
deleted file mode 100644
index b3241b5f..00000000
--- a/.ci_tools/run_samples.sh
+++ /dev/null
@@ -1,4 +0,0 @@
-#!/bin/bash
-
-# TBD: load apks on emulator
-#
diff --git a/.ci_tools/setup_env.sh b/.ci_tools/setup_env.sh
deleted file mode 100644
index 33c7b429..00000000
--- a/.ci_tools/setup_env.sh
+++ /dev/null
@@ -1,44 +0,0 @@
-#!/bin/bash
-
-# assumption:
-# - pwd must be inside the repo's homed directory (android-ndk)
-# - upon completion, we are still in the same directory ( no change )
-
-TMP_SETUP_FILENAME=versions_.txt
-
-# parse all build.gradle to find the specified tokens' version
-# usage:
-# retrive_versions token version_file
-# where
-# token: the token to search for inside build.grade
-# version string is right after the token string
-# version_file: file to hold the given versions
-# one version at a line
-retrieve_versions() {
-# $1: token; $2 version_file
- if [[ -z $1 ]] || [[ -z $2 ]]; then
- echo "input string(s) may be empty: token: $1; version_file: $2"
- return 1
- fi
-
- find . -type f -name 'build.gradle' -exec grep $1 {} + | \
- sed "s/^.*$1//" | sed 's/[=+]//g' | \
- sed 's/"//g' | sed "s/'//g" | \
- sed 's/[[:space:]]//g' | \
- awk '!seen[$0]++' > $2
-
- return 0
-}
-
-## Retrieve all necessary Android Platforms and install them all
-retrieve_versions compileSdkVersion $TMP_SETUP_FILENAME
-
-# fixups
-sed -i '/COMPILE_SDK_VERSION/d' $TMP_SETUP_FILENAME
-# Install platforms
-while read -r version_; do
- echo y | $ANDROID_HOME/tools/bin/sdkmanager "platforms;android-$version_";
-done < $TMP_SETUP_FILENAME
-#echo "Android platforms:"; cat $TMP_SETUP_FILENAME;rm -f $TMP_SETUP_FILENAME
-
-rm -f $TMP_SETUP_FILENAME
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index 053222f0..b1558121 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -37,4 +37,4 @@ for p in \
**Any additional context**
-If applicable, please attach a recording of the sound.
+If applicable, please attach a few seconds of an uncompressed recording of the sound in a WAV or AIFF file.
diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml
new file mode 100644
index 00000000..078c1cf8
--- /dev/null
+++ b/.github/workflows/android.yml
@@ -0,0 +1,37 @@
+name: Build CI
+
+on:
+ push:
+ branches: [ master ]
+ pull_request:
+ branches: [ master ]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v2
+ - name: set up JDK 1.8
+ uses: actions/setup-java@v1
+ with:
+ java-version: 1.8
+ - name: build samples
+ run: |
+ pushd samples
+ chmod +x gradlew
+ ./gradlew -q clean bundleDebug
+ popd
+ - name: build OboeTester
+ run: |
+ pushd apps/OboeTester
+ chmod +x gradlew
+ ./gradlew -q clean bundleDebug
+ popd
+ - name: build fxlab
+ run: |
+ pushd apps/fxlab
+ chmod +x gradlew
+ ./gradlew -q clean bundleDebug
+ popd
+
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 98549745..00000000
--- a/.travis.yml
+++ /dev/null
@@ -1,30 +0,0 @@
-language: android
-sudo: true
-android:
- components:
- - tools
- - platform-tools
- - extra-google-m2repository
- - extra-android-m2repository
-addons:
- apt_packages:
- - pandoc
-before_install:
- - sudo apt-get install ant
-install:
- - touch ~/.android/repositories.cfg
- - echo y | sdkmanager "ndk-bundle"
- - echo y | sdkmanager "cmake;3.6.4111459"
- # the following line triggers Trivis-CI's 4MB log limit
- # - sdkmanager --update
-before_script:
- - export ANDROID_NDK_HOME=$ANDROID_HOME/ndk-bundle
-
-script:
- # scripts excutes inside our repo directory on CI machine
- - export SAMPLE_CI_RESULT=0
- - source .ci_tools/setup_env.sh
- - source .ci_tools/build_samples.sh
- - source .ci_tools/run_samples.sh
- - source .ci_tools/misc_ci.sh
- - eval "[[ $SAMPLE_CI_RESULT == 0 ]]"
diff --git a/Android.bp b/Android.bp
index 9fae8a66..074054ff 100644
--- a/Android.bp
+++ b/Android.bp
@@ -86,11 +86,6 @@ cc_library_static {
"-Wno-deprecated-declarations",
"-Ofast",
"-DOBOE_NO_INCLUDE_AAUDIO",
- // work-around for Oboe issue: #1122
- "-DAAUDIO_OK=0",
- "-DAAUDIO_ERROR_TIMEOUT=-885",
- "-DAAUDIO_STREAM_STATE_STARTING=3",
- "-DAAUDIO_STREAM_STATE_STARTED=4"
],
sdk_version: "current",
stl: "libc++_static",
diff --git a/METADATA b/METADATA
index 4160dfef..0b3aae6c 100644
--- a/METADATA
+++ b/METADATA
@@ -5,10 +5,10 @@ third_party {
type: GIT
value: "https://github.com/google/oboe"
}
- version: "69c8eb17cf21621635634a111360d599360755a6"
+ version: "bab1c7de64aa1395c83be384056e2bad061ea272"
last_upgrade_date {
- year: 2020
- month: 11
- day: 30
+ year: 2021
+ month: 2
+ day: 9
}
}
diff --git a/README.md b/README.md
index a36ad6b1..9bdf2a02 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# Oboe [![Build Status](https://travis-ci.org/google/oboe.svg?branch=master)](https://travis-ci.org/google/oboe)
+# Oboe [![Build CI](https://github.com/google/oboe/workflows/Build%20CI/badge.svg)](https://github.com/google/oboe/actions)
[![Introduction to Oboe video](docs/images/getting-started-video.jpg)](https://www.youtube.com/watch?v=csfHAbr5ilI&list=PLWz5rJ2EKKc_duWv9IPNvx9YBudNMmLSa)
@@ -12,9 +12,6 @@ Oboe is a C++ library which makes it easy to build high-performance audio apps o
- Workarounds for some known issues
- [Used by popular apps and frameworks](docs/AppsUsingOboe.md)
-## Requirements
-To build Oboe you'll need a compiler which supports C++14 and the Android header files. The easiest way to obtain these is by downloading the Android NDK r17 or above. It can be installed using Android Studio's SDK manager, or via [direct download](https://developer.android.com/ndk/downloads/).
-
## Documentation
- [Getting Started Guide](docs/GettingStarted.md)
- [Full Guide to Oboe](docs/FullGuide.md)
@@ -25,6 +22,10 @@ To build Oboe you'll need a compiler which supports C++14 and the Android header
- [Frequently Asked Questions](docs/FAQ.md) (FAQ)
- [Our roadmap](https://github.com/google/oboe/milestones) - Vote on a feature/issue by adding a thumbs up to the first comment.
+### Community
+- Reddit: [r/androidaudiodev](https://www.reddit.com/r/androidaudiodev/)
+- StackOverflow: [#oboe](https://stackoverflow.com/questions/tagged/oboe)
+
## Testing
- [**OboeTester** app for measuring latency, glitches, etc.](https://github.com/google/oboe/tree/master/apps/OboeTester/docs)
- [Oboe unit tests](https://github.com/google/oboe/tree/master/tests)
@@ -32,9 +33,7 @@ To build Oboe you'll need a compiler which supports C++14 and the Android header
## Videos
- [Getting started with Oboe](https://www.youtube.com/playlist?list=PLWz5rJ2EKKc_duWv9IPNvx9YBudNMmLSa)
- [Low Latency Audio - Because Your Ears Are Worth It](https://www.youtube.com/watch?v=8vOf_fDtur4) (Android Dev Summit '18)
-- [Real-time audio with the 100 oscillator synthesizer](https://www.youtube.com/watch?v=J04iPJBkAKs) (DroidCon Berlin '18)
- [Winning on Android](https://www.youtube.com/watch?v=tWBojmBpS74) - How to optimize an Android audio app. (ADC '18)
-- [Real-Time Processing on Android](https://youtu.be/hY9BrS2uX-c) (ADC '19)
## Sample code and apps
- Sample apps can be found in the [samples directory](samples).
diff --git a/apps/OboeTester/app/CMakeLists.txt b/apps/OboeTester/app/CMakeLists.txt
index 8de21f9b..39cda1c9 100644
--- a/apps/OboeTester/app/CMakeLists.txt
+++ b/apps/OboeTester/app/CMakeLists.txt
@@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.4.1)
-set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Werror -Wall -std=c++14 -fvisibility=hidden")
+set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Werror -Wall -std=c++14 -DOBOE_NO_INCLUDE_AAUDIO -fvisibility=hidden")
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -O2")
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3")
@@ -31,4 +31,4 @@ include_directories(
# link to oboe
target_link_libraries(oboetester log oboe atomic)
-# bump 3 to resync CMake
+# bump 4 to resync CMake
diff --git a/apps/OboeTester/app/build.gradle b/apps/OboeTester/app/build.gradle
index dd01a160..f6e3fd5a 100644
--- a/apps/OboeTester/app/build.gradle
+++ b/apps/OboeTester/app/build.gradle
@@ -7,8 +7,8 @@ android {
minSdkVersion 23
targetSdkVersion 28
// Also update the versions in the AndroidManifest.xml file.
- versionCode 41
- versionName "1.5.33"
+ versionCode 44
+ versionName "1.6.2"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
externalNativeBuild {
cmake {
diff --git a/apps/OboeTester/app/src/main/AndroidManifest.xml b/apps/OboeTester/app/src/main/AndroidManifest.xml
index d158c452..b2469265 100644
--- a/apps/OboeTester/app/src/main/AndroidManifest.xml
+++ b/apps/OboeTester/app/src/main/AndroidManifest.xml
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.sample.oboe.manualtest"
- android:versionCode="41"
- android:versionName="1.5.33">
+ android:versionCode="44"
+ android:versionName="1.6.2">
<!-- versionCode and versionName also have to be updated in build.gradle -->
<uses-feature android:name="android.hardware.microphone" android:required="true" />
@@ -81,8 +81,8 @@
</activity>
<activity
- android:name="com.google.sample.oboe.manualtest.AutoGlitchActivity"
- android:label="@string/title_activity_glitches"
+ android:name="com.google.sample.oboe.manualtest.AutomatedGlitchActivity"
+ android:label="@string/title_activity_auto_glitches"
android:screenOrientation="portrait">
</activity>
@@ -98,6 +98,12 @@
android:screenOrientation="portrait">
</activity>
+ <activity
+ android:name="com.google.sample.oboe.manualtest.TestDataPathsActivity"
+ android:label="@string/title_data_paths"
+ android:screenOrientation="portrait">
+ </activity>
+
<service
android:name="com.google.sample.oboe.manualtest.AudioMidiTester"
android:permission="android.permission.BIND_MIDI_DEVICE_SERVICE">
diff --git a/apps/OboeTester/app/src/main/cpp/FullDuplexAnalyzer.h b/apps/OboeTester/app/src/main/cpp/FullDuplexAnalyzer.h
index ee12fc17..02271309 100644
--- a/apps/OboeTester/app/src/main/cpp/FullDuplexAnalyzer.h
+++ b/apps/OboeTester/app/src/main/cpp/FullDuplexAnalyzer.h
@@ -27,7 +27,10 @@
class FullDuplexAnalyzer : public FullDuplexStream {
public:
- FullDuplexAnalyzer() {}
+ FullDuplexAnalyzer(LoopbackProcessor *processor)
+ : mLoopbackProcessor(processor) {
+ setMNumInputBurstsCushion(1);
+ }
/**
* Called when data is available on both streams.
@@ -42,12 +45,10 @@ public:
oboe::Result start() override;
- bool isDone() {
- return false;
+ LoopbackProcessor *getLoopbackProcessor() {
+ return mLoopbackProcessor;
}
- virtual LoopbackProcessor *getLoopbackProcessor() = 0;
-
void setRecording(MultiChannelRecording *recording) {
mRecording = recording;
}
@@ -55,6 +56,7 @@ public:
private:
MultiChannelRecording *mRecording = nullptr;
+ LoopbackProcessor * const mLoopbackProcessor;
};
diff --git a/apps/OboeTester/app/src/main/cpp/FullDuplexGlitches.cpp b/apps/OboeTester/app/src/main/cpp/FullDuplexGlitches.cpp
deleted file mode 100644
index 53cfa5b2..00000000
--- a/apps/OboeTester/app/src/main/cpp/FullDuplexGlitches.cpp
+++ /dev/null
@@ -1,18 +0,0 @@
-/*
- * Copyright 2019 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 "common/OboeDebug.h"
-#include "FullDuplexGlitches.h"
diff --git a/apps/OboeTester/app/src/main/cpp/FullDuplexGlitches.h b/apps/OboeTester/app/src/main/cpp/FullDuplexGlitches.h
deleted file mode 100644
index 1046e04b..00000000
--- a/apps/OboeTester/app/src/main/cpp/FullDuplexGlitches.h
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * Copyright 2019 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 OBOETESTER_FULL_DUPLEX_GLITCHES_H
-#define OBOETESTER_FULL_DUPLEX_GLITCHES_H
-
-#include <unistd.h>
-#include <sys/types.h>
-
-#include "oboe/Oboe.h"
-#include "FullDuplexAnalyzer.h"
-#include "analyzer/GlitchAnalyzer.h"
-
-class FullDuplexGlitches : public FullDuplexAnalyzer {
-public:
- FullDuplexGlitches() {
- setMNumInputBurstsCushion(1);
- }
-
- bool isDone() {
- return false;
- }
-
- GlitchAnalyzer *getGlitchAnalyzer() {
- return &mGlitchAnalyzer;
- }
-
- LoopbackProcessor *getLoopbackProcessor() override {
- return (LoopbackProcessor *) &mGlitchAnalyzer;
- }
-
-private:
-
- GlitchAnalyzer mGlitchAnalyzer;
-
-};
-
-
-#endif //OBOETESTER_FULL_DUPLEX_GLITCHES_H
-
diff --git a/apps/OboeTester/app/src/main/cpp/FullDuplexLatency.cpp b/apps/OboeTester/app/src/main/cpp/FullDuplexLatency.cpp
deleted file mode 100644
index 34036782..00000000
--- a/apps/OboeTester/app/src/main/cpp/FullDuplexLatency.cpp
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * Copyright 2019 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 <thread>
-
-#include "common/OboeDebug.h"
-#include "FullDuplexLatency.h"
-
-static void analyze_data(FullDuplexLatency *fullDuplexLatency) {
- fullDuplexLatency->analyzeData();
-}
-
-oboe::DataCallbackResult FullDuplexLatency::onBothStreamsReady(
- const void *inputData,
- int numInputFrames,
- void *outputData,
- int numOutputFrames) {
-
- oboe::DataCallbackResult callbackResult = FullDuplexAnalyzer::onBothStreamsReady(
- inputData, numInputFrames, outputData, numOutputFrames);
-
- // Are we done?
- if (mEchoAnalyzer.hasEnoughData()) {
- // Crunch the numbers on a separate thread.
- std::thread t(analyze_data, this);
- t.detach();
- callbackResult = oboe::DataCallbackResult::Stop;
- }
-
- return callbackResult;
-};
diff --git a/apps/OboeTester/app/src/main/cpp/FullDuplexLatency.h b/apps/OboeTester/app/src/main/cpp/FullDuplexLatency.h
deleted file mode 100644
index a47217a9..00000000
--- a/apps/OboeTester/app/src/main/cpp/FullDuplexLatency.h
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * Copyright 2019 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 OBOETESTER_FULL_DUPLEX_LATENCY_H
-#define OBOETESTER_FULL_DUPLEX_LATENCY_H
-
-#include <unistd.h>
-#include <sys/types.h>
-
-#include "oboe/Oboe.h"
-#include "FullDuplexAnalyzer.h"
-
-class FullDuplexLatency : public FullDuplexAnalyzer {
-public:
- FullDuplexLatency() {}
-
- /**
- * Called when data is available on both streams.
- * Caller should override this method.
- */
- oboe::DataCallbackResult onBothStreamsReady(
- const void *inputData,
- int numInputFrames,
- void *outputData,
- int numOutputFrames
- ) override;
-
-
- bool isDone() {
- return mEchoAnalyzer.isDone();
- }
-
- void analyzeData() {
- mEchoAnalyzer.analyze();
- }
-
- LatencyAnalyzer *getLatencyAnalyzer() {
- return &mEchoAnalyzer;
- }
-
- LoopbackProcessor *getLoopbackProcessor() override {
- return (LoopbackProcessor *) &mEchoAnalyzer;
- }
-
-private:
-
- PulseLatencyAnalyzer mEchoAnalyzer;
-
-};
-
-
-#endif //OBOETESTER_FULL_DUPLEX_LATENCY_H
diff --git a/apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp b/apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp
index 7809206f..c95e0933 100644
--- a/apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp
+++ b/apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp
@@ -17,6 +17,7 @@
#include <fstream>
#include <iostream>
#include <vector>
+#include <common/AudioClock.h>
#include "util/WaveFileWriter.h"
@@ -57,9 +58,9 @@ private:
bool ActivityContext::mUseCallback = true;
int ActivityContext::callbackSize = 0;
-oboe::AudioStream * ActivityContext::getOutputStream() {
+std::shared_ptr<oboe::AudioStream> ActivityContext::getOutputStream() {
for (auto entry : mOboeStreams) {
- oboe::AudioStream *oboeStream = entry.second.get();
+ std::shared_ptr<oboe::AudioStream> oboeStream = entry.second;
if (oboeStream->getDirection() == oboe::Direction::Output) {
return oboeStream;
}
@@ -67,9 +68,9 @@ oboe::AudioStream * ActivityContext::getOutputStream() {
return nullptr;
}
-oboe::AudioStream * ActivityContext::getInputStream() {
+std::shared_ptr<oboe::AudioStream> ActivityContext::getInputStream() {
for (auto entry : mOboeStreams) {
- oboe::AudioStream *oboeStream = entry.second.get();
+ std::shared_ptr<oboe::AudioStream> oboeStream = entry.second;
if (oboeStream != nullptr) {
if (oboeStream->getDirection() == oboe::Direction::Input) {
return oboeStream;
@@ -90,7 +91,7 @@ int32_t ActivityContext::allocateStreamIndex() {
void ActivityContext::close(int32_t streamIndex) {
stopBlockingIOThread();
- oboe::AudioStream *oboeStream = getStream(streamIndex);
+ std::shared_ptr<oboe::AudioStream> oboeStream = getStream(streamIndex);
if (oboeStream != nullptr) {
oboeStream->close();
LOGD("ActivityContext::%s() delete stream %d ", __func__, streamIndex);
@@ -99,17 +100,17 @@ void ActivityContext::close(int32_t streamIndex) {
}
bool ActivityContext::isMMapUsed(int32_t streamIndex) {
- oboe::AudioStream *oboeStream = getStream(streamIndex);
+ std::shared_ptr<oboe::AudioStream> oboeStream = getStream(streamIndex);
if (oboeStream == nullptr) return false;
if (oboeStream->getAudioApi() != AudioApi::AAudio) return false;
- return AAudioExtensions::getInstance().isMMapUsed(oboeStream);
+ return AAudioExtensions::getInstance().isMMapUsed(oboeStream.get());
}
oboe::Result ActivityContext::pause() {
oboe::Result result = oboe::Result::OK;
stopBlockingIOThread();
for (auto entry : mOboeStreams) {
- oboe::AudioStream *oboeStream = entry.second.get();
+ std::shared_ptr<oboe::AudioStream> oboeStream = entry.second;
result = oboeStream->requestPause();
}
return result;
@@ -119,7 +120,7 @@ oboe::Result ActivityContext::stopAllStreams() {
oboe::Result result = oboe::Result::OK;
stopBlockingIOThread();
for (auto entry : mOboeStreams) {
- oboe::AudioStream *oboeStream = entry.second.get();
+ std::shared_ptr<oboe::AudioStream> oboeStream = entry.second;
result = oboeStream->requestStop();
}
return result;
@@ -197,6 +198,12 @@ int ActivityContext::open(jint nativeApi,
bool oldMMapEnabled = AAudioExtensions::getInstance().isMMapEnabled();
AAudioExtensions::getInstance().setMMapEnabled(isMMap);
+ // Record time for opening.
+ if (isInput) {
+ mInputOpenedAt = oboe::AudioClock::getNanoseconds();
+ } else {
+ mOutputOpenedAt = oboe::AudioClock::getNanoseconds();
+ }
// Open a stream based on the builder settings.
std::shared_ptr<oboe::AudioStream> oboeStream;
Result result = builder.openStream(oboeStream);
@@ -226,8 +233,8 @@ int ActivityContext::open(jint nativeApi,
oboe::Result ActivityContext::start() {
oboe::Result result = oboe::Result::OK;
- oboe::AudioStream *inputStream = getInputStream();
- oboe::AudioStream *outputStream = getOutputStream();
+ std::shared_ptr<oboe::AudioStream> inputStream = getInputStream();
+ std::shared_ptr<oboe::AudioStream> outputStream = getOutputStream();
if (inputStream == nullptr && outputStream == nullptr) {
LOGD("%s() - no streams defined", __func__);
return oboe::Result::ErrorInvalidState; // not open
@@ -282,6 +289,15 @@ int32_t ActivityContext::saveWaveFile(const char *filename) {
return outStream.length();
}
+double ActivityContext::getTimestampLatency(int32_t streamIndex) {
+ std::shared_ptr<oboe::AudioStream> oboeStream = getStream(streamIndex);
+ if (oboeStream != nullptr) {
+ auto result = oboeStream->calculateLatencyMillis();
+ return (!result) ? -1.0 : result.value();
+ }
+ return -1.0;
+}
+
// =================================================================== ActivityTestOutput
void ActivityTestOutput::close(int32_t streamIndex) {
ActivityContext::close(streamIndex);
@@ -326,7 +342,7 @@ void ActivityTestOutput::configureForStart() {
mSinkFloat = std::make_unique<SinkFloat>(mChannelCount);
mSinkI16 = std::make_unique<SinkI16>(mChannelCount);
- oboe::AudioStream *outputStream = getOutputStream();
+ std::shared_ptr<oboe::AudioStream> outputStream = getOutputStream();
mTriangleOscillator.setSampleRate(outputStream->getSampleRate());
mTriangleOscillator.frequency.setValue(1.0/kSweepPeriod);
@@ -362,7 +378,7 @@ void ActivityTestOutput::configureForStart() {
}
void ActivityTestOutput::configureStreamGateway() {
- oboe::AudioStream *outputStream = getOutputStream();
+ std::shared_ptr<oboe::AudioStream> outputStream = getOutputStream();
if (outputStream->getFormat() == oboe::AudioFormat::I16) {
audioStreamGateway.setAudioSink(mSinkI16);
} else if (outputStream->getFormat() == oboe::AudioFormat::Float) {
@@ -378,7 +394,7 @@ void ActivityTestOutput::runBlockingIO() {
int32_t framesPerBlock = getFramesPerBlock();
oboe::DataCallbackResult callbackResult = oboe::DataCallbackResult::Continue;
- oboe::AudioStream *oboeStream = getOutputStream();
+ std::shared_ptr<oboe::AudioStream> oboeStream = getOutputStream();
if (oboeStream == nullptr) {
LOGE("%s() : no stream found\n", __func__);
return;
@@ -387,7 +403,7 @@ void ActivityTestOutput::runBlockingIO() {
while (threadEnabled.load()
&& callbackResult == oboe::DataCallbackResult::Continue) {
// generate output by calling the callback
- callbackResult = audioStreamGateway.onAudioReady(oboeStream,
+ callbackResult = audioStreamGateway.onAudioReady(oboeStream.get(),
dataBuffer.get(),
framesPerBlock);
@@ -420,7 +436,7 @@ void ActivityTestInput::runBlockingIO() {
int32_t framesPerBlock = getFramesPerBlock();
oboe::DataCallbackResult callbackResult = oboe::DataCallbackResult::Continue;
- oboe::AudioStream *oboeStream = getInputStream();
+ std::shared_ptr<oboe::AudioStream> oboeStream = getInputStream();
if (oboeStream == nullptr) {
LOGE("%s() : no stream found\n", __func__);
return;
@@ -448,7 +464,7 @@ void ActivityTestInput::runBlockingIO() {
}
// analyze input
- callbackResult = mInputAnalyzer.onAudioReady(oboeStream,
+ callbackResult = mInputAnalyzer.onAudioReady(oboeStream.get(),
dataBuffer.get(),
framesRead);
}
@@ -495,7 +511,7 @@ void ActivityTapToTone::configureForStart() {
mSinkFloat = std::make_unique<SinkFloat>(mChannelCount);
mSinkI16 = std::make_unique<SinkI16>(mChannelCount);
- oboe::AudioStream *outputStream = getOutputStream();
+ std::shared_ptr<oboe::AudioStream> outputStream = getOutputStream();
sawPingGenerator.setSampleRate(outputStream->getSampleRate());
sawPingGenerator.frequency.setValue(FREQUENCY_SAW_PING);
sawPingGenerator.amplitude.setValue(AMPLITUDE_SAW_PING);
@@ -514,7 +530,7 @@ void ActivityTapToTone::configureForStart() {
void ActivityFullDuplex::configureBuilder(bool isInput, oboe::AudioStreamBuilder &builder) {
if (isInput) {
// Ideally the output streams should be opened first.
- oboe::AudioStream *outputStream = getOutputStream();
+ std::shared_ptr<oboe::AudioStream> outputStream = getOutputStream();
if (outputStream != nullptr) {
// Make sure the capacity is bigger than two bursts.
int32_t burst = outputStream->getFramesPerBurst();
@@ -549,7 +565,7 @@ void ActivityRoundTripLatency::configureBuilder(bool isInput, oboe::AudioStreamB
ActivityFullDuplex::configureBuilder(isInput, builder);
if (mFullDuplexLatency.get() == nullptr) {
- mFullDuplexLatency = std::make_unique<FullDuplexLatency>();
+ mFullDuplexLatency = std::make_unique<FullDuplexAnalyzer>(&mEchoAnalyzer);
}
if (!isInput) {
// only output uses a callback, input is polled
@@ -557,7 +573,7 @@ void ActivityRoundTripLatency::configureBuilder(bool isInput, oboe::AudioStreamB
}
}
-void ActivityRoundTripLatency::finishOpen(bool isInput, oboe::AudioStream *oboeStream) {
+void ActivityRoundTripLatency::finishOpen(bool isInput, AudioStream *oboeStream) {
if (isInput) {
mFullDuplexLatency->setInputStream(oboeStream);
mFullDuplexLatency->setRecording(mRecording.get());
@@ -571,7 +587,7 @@ void ActivityGlitches::configureBuilder(bool isInput, oboe::AudioStreamBuilder &
ActivityFullDuplex::configureBuilder(isInput, builder);
if (mFullDuplexGlitches.get() == nullptr) {
- mFullDuplexGlitches = std::make_unique<FullDuplexGlitches>();
+ mFullDuplexGlitches = std::make_unique<FullDuplexAnalyzer>(&mGlitchAnalyzer);
}
if (!isInput) {
// only output uses a callback, input is polled
@@ -588,6 +604,27 @@ void ActivityGlitches::finishOpen(bool isInput, oboe::AudioStream *oboeStream) {
}
}
+// ======================================================================= ActivityDataPath
+void ActivityDataPath::configureBuilder(bool isInput, oboe::AudioStreamBuilder &builder) {
+ ActivityFullDuplex::configureBuilder(isInput, builder);
+
+ if (mFullDuplexDataPath.get() == nullptr) {
+ mFullDuplexDataPath = std::make_unique<FullDuplexAnalyzer>(&mDataPathAnalyzer);
+ }
+ if (!isInput) {
+ // only output uses a callback, input is polled
+ builder.setCallback(mFullDuplexDataPath.get());
+ }
+}
+
+void ActivityDataPath::finishOpen(bool isInput, oboe::AudioStream *oboeStream) {
+ if (isInput) {
+ mFullDuplexDataPath->setInputStream(oboeStream);
+ mFullDuplexDataPath->setRecording(mRecording.get());
+ } else {
+ mFullDuplexDataPath->setOutputStream(oboeStream);
+ }
+}
// =================================================================== ActivityTestDisconnect
void ActivityTestDisconnect::close(int32_t streamIndex) {
@@ -596,8 +633,8 @@ void ActivityTestDisconnect::close(int32_t streamIndex) {
}
void ActivityTestDisconnect::configureForStart() {
- oboe::AudioStream *outputStream = getOutputStream();
- oboe::AudioStream *inputStream = getInputStream();
+ std::shared_ptr<oboe::AudioStream> outputStream = getOutputStream();
+ std::shared_ptr<oboe::AudioStream> inputStream = getInputStream();
if (outputStream) {
mSinkFloat = std::make_unique<SinkFloat>(mChannelCount);
sineOscillator = std::make_unique<SineOscillator>();
diff --git a/apps/OboeTester/app/src/main/cpp/NativeAudioContext.h b/apps/OboeTester/app/src/main/cpp/NativeAudioContext.h
index 95df9849..85735a4d 100644
--- a/apps/OboeTester/app/src/main/cpp/NativeAudioContext.h
+++ b/apps/OboeTester/app/src/main/cpp/NativeAudioContext.h
@@ -17,7 +17,6 @@
#ifndef NATIVEOBOE_NATIVEAUDIOCONTEXT_H
#define NATIVEOBOE_NATIVEAUDIOCONTEXT_H
-#include <dlfcn.h>
#include <jni.h>
#include <sys/system_properties.h>
#include <thread>
@@ -27,7 +26,9 @@
#include "common/OboeDebug.h"
#include "oboe/Oboe.h"
+#include "aaudio/AAudioExtensions.h"
#include "AudioStreamGateway.h"
+
#include "flowunits/ImpulseOscillator.h"
#include "flowgraph/ManyToMultiConverter.h"
#include "flowgraph/MonoToMultiConverter.h"
@@ -37,17 +38,18 @@
#include "flowunits/LinearShape.h"
#include "flowunits/SineOscillator.h"
#include "flowunits/SawtoothOscillator.h"
+#include "flowunits/TriangleOscillator.h"
+#include "FullDuplexAnalyzer.h"
#include "FullDuplexEcho.h"
-#include "FullDuplexGlitches.h"
-#include "FullDuplexLatency.h"
#include "FullDuplexStream.h"
+#include "analyzer/GlitchAnalyzer.h"
+#include "analyzer/DataPathAnalyzer.h"
#include "InputStreamCallbackAnalyzer.h"
#include "MultiChannelRecording.h"
#include "OboeStreamCallbackProxy.h"
#include "PlayRecordingCallback.h"
#include "SawPingGenerator.h"
-#include "flowunits/TriangleOscillator.h"
// These must match order in strings.xml and in StreamConfiguration.java
#define NATIVE_MODE_UNSPECIFIED 0
@@ -65,126 +67,8 @@
#define NANOS_PER_MILLISECOND (1000 * NANOS_PER_MICROSECOND)
#define NANOS_PER_SECOND (1000 * NANOS_PER_MILLISECOND)
-#define LIB_AAUDIO_NAME "libaaudio.so"
-#define FUNCTION_IS_MMAP "AAudioStream_isMMapUsed"
-#define FUNCTION_SET_MMAP_POLICY "AAudio_setMMapPolicy"
-#define FUNCTION_GET_MMAP_POLICY "AAudio_getMMapPolicy"
-
#define SECONDS_TO_RECORD 10
-typedef struct AAudioStreamStruct AAudioStream;
-
-/**
- * Call some AAudio test routines that are not part of the normal API.
- */
-class AAudioExtensions {
-public:
- AAudioExtensions() {
- int32_t policy = getIntegerProperty("aaudio.mmap_policy", 0);
- mMMapSupported = isPolicyEnabled(policy);
-
- policy = getIntegerProperty("aaudio.mmap_exclusive_policy", 0);
- mMMapExclusiveSupported = isPolicyEnabled(policy);
- }
-
- static bool isPolicyEnabled(int32_t policy) {
- return (policy == AAUDIO_POLICY_AUTO || policy == AAUDIO_POLICY_ALWAYS);
- }
-
- static AAudioExtensions &getInstance() {
- static AAudioExtensions instance;
- return instance;
- }
-
- bool isMMapUsed(oboe::AudioStream *oboeStream) {
- if (!loadLibrary()) return false;
- if (mAAudioStream_isMMap == nullptr) return false;
- AAudioStream *aaudioStream = (AAudioStream *) oboeStream->getUnderlyingStream();
- return mAAudioStream_isMMap(aaudioStream);
- }
-
- bool setMMapEnabled(bool enabled) {
- if (!loadLibrary()) return false;
- if (mAAudio_setMMapPolicy == nullptr) return false;
- return mAAudio_setMMapPolicy(enabled ? AAUDIO_POLICY_AUTO : AAUDIO_POLICY_NEVER);
- }
-
- bool isMMapEnabled() {
- if (!loadLibrary()) return false;
- if (mAAudio_getMMapPolicy == nullptr) return false;
- int32_t policy = mAAudio_getMMapPolicy();
- return isPolicyEnabled(policy);
- }
-
- bool isMMapSupported() {
- return mMMapSupported;
- }
-
- bool isMMapExclusiveSupported() {
- return mMMapExclusiveSupported;
- }
-
-private:
-
- enum {
- AAUDIO_POLICY_NEVER = 1,
- AAUDIO_POLICY_AUTO,
- AAUDIO_POLICY_ALWAYS
- };
- typedef int32_t aaudio_policy_t;
-
- int getIntegerProperty(const char *name, int defaultValue) {
- int result = defaultValue;
- char valueText[PROP_VALUE_MAX] = {0};
- if (__system_property_get(name, valueText) != 0) {
- result = atoi(valueText);
- }
- return result;
- }
-
- // return true if it succeeds
- bool loadLibrary() {
- if (mFirstTime) {
- mFirstTime = false;
- mLibHandle = dlopen(LIB_AAUDIO_NAME, 0);
- if (mLibHandle == nullptr) {
- LOGI("%s() could not find " LIB_AAUDIO_NAME, __func__);
- return false;
- }
-
- mAAudioStream_isMMap = (bool (*)(AAudioStream *stream))
- dlsym(mLibHandle, FUNCTION_IS_MMAP);
- if (mAAudioStream_isMMap == nullptr) {
- LOGI("%s() could not find " FUNCTION_IS_MMAP, __func__);
- return false;
- }
-
- mAAudio_setMMapPolicy = (int32_t (*)(aaudio_policy_t policy))
- dlsym(mLibHandle, FUNCTION_SET_MMAP_POLICY);
- if (mAAudio_setMMapPolicy == nullptr) {
- LOGI("%s() could not find " FUNCTION_SET_MMAP_POLICY, __func__);
- return false;
- }
-
- mAAudio_getMMapPolicy = (aaudio_policy_t (*)())
- dlsym(mLibHandle, FUNCTION_GET_MMAP_POLICY);
- if (mAAudio_getMMapPolicy == nullptr) {
- LOGI("%s() could not find " FUNCTION_GET_MMAP_POLICY, __func__);
- return false;
- }
- }
- return (mLibHandle != nullptr);
- }
-
- bool mFirstTime = true;
- void *mLibHandle = nullptr;
- bool (*mAAudioStream_isMMap)(AAudioStream *stream) = nullptr;
- int32_t (*mAAudio_setMMapPolicy)(aaudio_policy_t policy) = nullptr;
- aaudio_policy_t (*mAAudio_getMMapPolicy)() = nullptr;
- bool mMMapSupported = false;
- bool mMMapExclusiveSupported = false;
-};
-
/**
* Abstract base class that corresponds to a test at the Java level.
*/
@@ -195,10 +79,10 @@ public:
virtual ~ActivityContext() = default;
- oboe::AudioStream *getStream(int32_t streamIndex) {
+ std::shared_ptr<oboe::AudioStream> getStream(int32_t streamIndex) {
auto it = mOboeStreams.find(streamIndex);
if (it != mOboeStreams.end()) {
- return it->second.get();
+ return it->second;
} else {
return nullptr;
}
@@ -206,6 +90,25 @@ public:
virtual void configureBuilder(bool isInput, oboe::AudioStreamBuilder &builder);
+ /**
+ * Open a stream with the given parameters.
+ * @param nativeApi
+ * @param sampleRate
+ * @param channelCount
+ * @param format
+ * @param sharingMode
+ * @param performanceMode
+ * @param inputPreset
+ * @param deviceId
+ * @param sessionId
+ * @param framesPerBurst
+ * @param channelConversionAllowed
+ * @param formatConversionAllowed
+ * @param rateConversionQuality
+ * @param isMMap
+ * @param isInput
+ * @return stream ID
+ */
int open(jint nativeApi,
jint sampleRate,
jint channelCount,
@@ -222,7 +125,6 @@ public:
jboolean isMMap,
jboolean isInput);
-
virtual void close(int32_t streamIndex);
virtual void configureForStart() {}
@@ -272,6 +174,60 @@ public:
return 0.0;
}
+ static int64_t getNanoseconds(clockid_t clockId = CLOCK_MONOTONIC) {
+ struct timespec time;
+ int result = clock_gettime(clockId, &time);
+ if (result < 0) {
+ return result;
+ }
+ return (time.tv_sec * NANOS_PER_SECOND) + time.tv_nsec;
+ }
+
+ // Calculate time between beginning and when frame[0] occurred.
+ int32_t calculateColdStartLatencyMillis(int32_t sampleRate,
+ int64_t beginTimeNanos,
+ int64_t timeStampPosition,
+ int64_t timestampNanos) const {
+ int64_t elapsedNanos = NANOS_PER_SECOND * (timeStampPosition / (double) sampleRate);
+ int64_t timeOfFrameZero = timestampNanos - elapsedNanos;
+ int64_t coldStartLatencyNanos = timeOfFrameZero - beginTimeNanos;
+ return coldStartLatencyNanos / NANOS_PER_MILLISECOND;
+ }
+
+ int32_t getColdStartInputMillis() {
+ std::shared_ptr<oboe::AudioStream> oboeStream = getInputStream();
+ if (oboeStream != nullptr) {
+ int64_t framesRead = oboeStream->getFramesRead();
+ if (framesRead > 0) {
+ // Base latency on the time that frame[0] would have been received by the app.
+ int64_t nowNanos = getNanoseconds();
+ return calculateColdStartLatencyMillis(oboeStream->getSampleRate(),
+ mInputOpenedAt,
+ framesRead,
+ nowNanos);
+ }
+ }
+ return -1;
+ }
+
+ int32_t getColdStartOutputMillis() {
+ std::shared_ptr<oboe::AudioStream> oboeStream = getOutputStream();
+ if (oboeStream != nullptr) {
+ auto result = oboeStream->getTimestamp(CLOCK_MONOTONIC);
+ if (result) {
+ auto frameTimestamp = result.value();
+ // Calculate the time that frame[0] would have been played by the speaker.
+ int64_t position = frameTimestamp.position;
+ int64_t timestampNanos = frameTimestamp.timestamp;
+ return calculateColdStartLatencyMillis(oboeStream->getSampleRate(),
+ mOutputOpenedAt,
+ position,
+ timestampNanos);
+ }
+ }
+ return -1;
+ }
+
/**
* Trigger a sound or impulse.
* @param enabled
@@ -289,7 +245,7 @@ public:
}
oboe::Result getLastErrorCallbackResult() {
- oboe::AudioStream *stream = getOutputStream();
+ std::shared_ptr<oboe::AudioStream> stream = getOutputStream();
if (stream == nullptr) {
stream = getInputStream();
}
@@ -311,9 +267,11 @@ public:
static bool mUseCallback;
static int callbackSize;
+ double getTimestampLatency(int32_t streamIndex);
+
protected:
- oboe::AudioStream *getInputStream();
- oboe::AudioStream *getOutputStream();
+ std::shared_ptr<oboe::AudioStream> getInputStream();
+ std::shared_ptr<oboe::AudioStream> getOutputStream();
int32_t allocateStreamIndex();
void freeStreamIndex(int32_t streamIndex);
@@ -343,6 +301,8 @@ protected:
std::thread *dataThread = nullptr;
private:
+ int64_t mInputOpenedAt = 0;
+ int64_t mOutputOpenedAt = 0;
};
/**
@@ -548,34 +508,58 @@ class ActivityRoundTripLatency : public ActivityFullDuplex {
public:
oboe::Result startStreams() override {
+ mAnalyzerLaunched = false;
return mFullDuplexLatency->start();
}
void configureBuilder(bool isInput, oboe::AudioStreamBuilder &builder) override;
LatencyAnalyzer *getLatencyAnalyzer() {
- return mFullDuplexLatency->getLatencyAnalyzer();
+ return &mEchoAnalyzer;
}
int32_t getState() override {
return getLatencyAnalyzer()->getState();
}
+
int32_t getResult() override {
- return getLatencyAnalyzer()->getState();
+ return getLatencyAnalyzer()->getState(); // TODO This does not look right.
}
+
bool isAnalyzerDone() override {
- return mFullDuplexLatency->isDone();
+ if (!mAnalyzerLaunched) {
+ mAnalyzerLaunched = launchAnalysisIfReady();
+ }
+ return mEchoAnalyzer.isDone();
}
FullDuplexAnalyzer *getFullDuplexAnalyzer() override {
return (FullDuplexAnalyzer *) mFullDuplexLatency.get();
}
+ static void analyzeData(PulseLatencyAnalyzer *analyzer) {
+ analyzer->analyze();
+ }
+
+ bool launchAnalysisIfReady() {
+ // Are we ready to do the analysis?
+ if (mEchoAnalyzer.hasEnoughData()) {
+ // Crunch the numbers on a separate thread.
+ std::thread t(analyzeData, &mEchoAnalyzer);
+ t.detach();
+ return true;
+ }
+ return false;
+ }
+
protected:
void finishOpen(bool isInput, oboe::AudioStream *oboeStream) override;
private:
- std::unique_ptr<FullDuplexLatency> mFullDuplexLatency{};
+ std::unique_ptr<FullDuplexAnalyzer> mFullDuplexLatency{};
+
+ PulseLatencyAnalyzer mEchoAnalyzer;
+ bool mAnalyzerLaunched = false;
};
/**
@@ -591,18 +575,19 @@ public:
void configureBuilder(bool isInput, oboe::AudioStreamBuilder &builder) override;
GlitchAnalyzer *getGlitchAnalyzer() {
- if (!mFullDuplexGlitches) return nullptr;
- return mFullDuplexGlitches->getGlitchAnalyzer();
+ return &mGlitchAnalyzer;
}
int32_t getState() override {
return getGlitchAnalyzer()->getState();
}
+
int32_t getResult() override {
return getGlitchAnalyzer()->getResult();
}
+
bool isAnalyzerDone() override {
- return mFullDuplexGlitches->isDone();
+ return mGlitchAnalyzer.isDone();
}
FullDuplexAnalyzer *getFullDuplexAnalyzer() override {
@@ -613,7 +598,50 @@ protected:
void finishOpen(bool isInput, oboe::AudioStream *oboeStream) override;
private:
- std::unique_ptr<FullDuplexGlitches> mFullDuplexGlitches{};
+ std::unique_ptr<FullDuplexAnalyzer> mFullDuplexGlitches{};
+ GlitchAnalyzer mGlitchAnalyzer;
+};
+
+/**
+ * Measure Data Path
+ */
+class ActivityDataPath : public ActivityFullDuplex {
+public:
+
+ oboe::Result startStreams() override {
+ return mFullDuplexDataPath->start();
+ }
+
+ void configureBuilder(bool isInput, oboe::AudioStreamBuilder &builder) override;
+
+ void configureForStart() override {
+ std::shared_ptr<oboe::AudioStream> outputStream = getOutputStream();
+ int32_t capacityInFrames = outputStream->getBufferCapacityInFrames();
+ int32_t burstInFrames = outputStream->getFramesPerBurst();
+ int32_t capacityInBursts = capacityInFrames / burstInFrames;
+ int32_t sizeInBursts = std::max(2, capacityInBursts / 2);
+ // Set size of buffer to minimize underruns.
+ auto result = outputStream->setBufferSizeInFrames(sizeInBursts * burstInFrames);
+ static_cast<void>(result); // Avoid unused variable.
+ LOGD("ActivityDataPath: %s() capacity = %d, burst = %d, size = %d",
+ __func__, capacityInFrames, burstInFrames, result.value());
+ }
+
+ DataPathAnalyzer *getDataPathAnalyzer() {
+ return &mDataPathAnalyzer;
+ }
+
+ FullDuplexAnalyzer *getFullDuplexAnalyzer() override {
+ return (FullDuplexAnalyzer *) mFullDuplexDataPath.get();
+ }
+
+protected:
+ void finishOpen(bool isInput, oboe::AudioStream *oboeStream) override;
+
+private:
+ std::unique_ptr<FullDuplexAnalyzer> mFullDuplexDataPath{};
+
+ DataPathAnalyzer mDataPathAnalyzer;
};
/**
@@ -628,12 +656,12 @@ public:
void close(int32_t streamIndex) override;
oboe::Result startStreams() override {
- oboe::AudioStream *outputStream = getOutputStream();
+ std::shared_ptr<oboe::AudioStream> outputStream = getOutputStream();
if (outputStream) {
return outputStream->start();
}
- oboe::AudioStream *inputStream = getInputStream();
+ std::shared_ptr<oboe::AudioStream> inputStream = getInputStream();
if (inputStream) {
return inputStream->start();
}
@@ -687,6 +715,9 @@ public:
case ActivityType::TestDisconnect:
currentActivity = &mActivityTestDisconnect;
break;
+ case ActivityType::DataPath:
+ currentActivity = &mActivityDataPath;
+ break;
}
}
@@ -701,8 +732,10 @@ public:
ActivityEcho mActivityEcho;
ActivityRoundTripLatency mActivityRoundTripLatency;
ActivityGlitches mActivityGlitches;
+ ActivityDataPath mActivityDataPath;
ActivityTestDisconnect mActivityTestDisconnect;
+
private:
// WARNING - must match definitions in TestAudioActivity.java
@@ -716,6 +749,7 @@ private:
RoundTripLatency = 5,
Glitches = 6,
TestDisconnect = 7,
+ DataPath = 8,
};
ActivityType mActivityType = ActivityType::Undefined;
diff --git a/apps/OboeTester/app/src/main/cpp/analyzer/BaseSineAnalyzer.h b/apps/OboeTester/app/src/main/cpp/analyzer/BaseSineAnalyzer.h
new file mode 100644
index 00000000..6601f19b
--- /dev/null
+++ b/apps/OboeTester/app/src/main/cpp/analyzer/BaseSineAnalyzer.h
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2020 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 ANALYZER_BASE_SINE_ANALYZER_H
+#define ANALYZER_BASE_SINE_ANALYZER_H
+
+#include <algorithm>
+#include <cctype>
+#include <iomanip>
+#include <iostream>
+
+#include "InfiniteRecording.h"
+#include "LatencyAnalyzer.h"
+
+/**
+ * Output a steady sine wave and analyze the return signal.
+ *
+ * Use a cosine transform to measure the predicted magnitude and relative phase of the
+ * looped back sine wave. Then generate a predicted signal and compare with the actual signal.
+ */
+class BaseSineAnalyzer : public LoopbackProcessor {
+public:
+
+ BaseSineAnalyzer()
+ : LoopbackProcessor()
+ , mInfiniteRecording(64 * 1024) {}
+
+
+ virtual bool isOutputEnabled() { return true; }
+
+ void setMagnitude(double magnitude) {
+ mMagnitude = magnitude;
+ mScaledTolerance = mMagnitude * mTolerance;
+ }
+
+ double getPhaseOffset() {
+ return mPhaseOffset;
+ }
+
+ double getMagnitude() const {
+ return mMagnitude;
+ }
+
+ void setInputChannel(int inputChannel) {
+ mInputChannel = inputChannel;
+ }
+
+ int getInputChannel() const {
+ return mInputChannel;
+ }
+
+ void setOutputChannel(int outputChannel) {
+ mOutputChannel = outputChannel;
+ }
+
+ int getOutputChannel() const {
+ return mOutputChannel;
+ }
+
+ void setNoiseAmplitude(double noiseAmplitude) {
+ mNoiseAmplitude = noiseAmplitude;
+ }
+
+ double getNoiseAmplitude() const {
+ return mNoiseAmplitude;
+ }
+
+ double getTolerance() {
+ return mTolerance;
+ }
+
+ void setTolerance(double tolerance) {
+ mTolerance = tolerance;
+ }
+
+ // advance and wrap phase
+ void incrementOutputPhase() {
+ mOutputPhase += mPhaseIncrement;
+ if (mOutputPhase > M_PI) {
+ mOutputPhase -= (2.0 * M_PI);
+ }
+ }
+
+ /**
+ * @param frameData upon return, contains the reference sine wave
+ * @param channelCount
+ */
+ result_code processOutputFrame(float *frameData, int channelCount) override {
+ float output = 0.0f;
+ // Output sine wave so we can measure it.
+ if (isOutputEnabled()) {
+ float sinOut = sinf(mOutputPhase);
+ incrementOutputPhase();
+ output = (sinOut * mOutputAmplitude)
+ + (mWhiteNoise.nextRandomDouble() * mNoiseAmplitude);
+ // ALOGD("sin(%f) = %f, %f\n", mOutputPhase, sinOut, mPhaseIncrement);
+ }
+ for (int i = 0; i < channelCount; i++) {
+ frameData[i] = (i == mOutputChannel) ? output : 0.0f;
+ }
+ return RESULT_OK;
+ }
+
+ /**
+ * Calculate the magnitude of the component of the input signal
+ * that matches the analysis frequency.
+ * Also calculate the phase that we can use to create a
+ * signal that matches that component.
+ * The phase will be between -PI and +PI.
+ */
+ double calculateMagnitudePhase(double *phasePtr = nullptr) {
+ if (mFramesAccumulated == 0) {
+ return 0.0;
+ }
+ double sinMean = mSinAccumulator / mFramesAccumulated;
+ double cosMean = mCosAccumulator / mFramesAccumulated;
+ double magnitude = 2.0 * sqrt((sinMean * sinMean) + (cosMean * cosMean));
+ if (phasePtr != nullptr) {
+ double phase = M_PI_2 - atan2(sinMean, cosMean);
+ *phasePtr = phase;
+ }
+ return magnitude;
+ }
+
+ bool transformSample(float sample, float referencePhase) {
+ // Track incoming signal and slowly adjust magnitude to account
+ // for drift in the DRC or AGC.
+ mSinAccumulator += sample * sinf(referencePhase);
+ mCosAccumulator += sample * cosf(referencePhase);
+ mFramesAccumulated++;
+ // Must be a multiple of the period or the calculation will not be accurate.
+ if (mFramesAccumulated == mSinePeriod) {
+ const double coefficient = 0.1;
+ double magnitude = calculateMagnitudePhase(&mPhaseOffset);
+ // One pole averaging filter.
+ setMagnitude((mMagnitude * (1.0 - coefficient)) + (magnitude * coefficient));
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ // reset the sine wave detector
+ virtual void resetAccumulator() {
+ mFramesAccumulated = 0;
+ mSinAccumulator = 0.0;
+ mCosAccumulator = 0.0;
+ }
+
+ void reset() override {
+ LoopbackProcessor::reset();
+ resetAccumulator();
+ }
+
+ void prepareToTest() override {
+ LoopbackProcessor::prepareToTest();
+ mSinePeriod = getSampleRate() / kTargetGlitchFrequency;
+ mOutputPhase = 0.0f;
+ mInverseSinePeriod = 1.0 / mSinePeriod;
+ mPhaseIncrement = 2.0 * M_PI * mInverseSinePeriod;
+ }
+
+protected:
+ static constexpr int32_t kTargetGlitchFrequency = 1000;
+
+ int32_t mSinePeriod = 1; // this will be set before use
+ double mInverseSinePeriod = 1.0;
+ double mPhaseIncrement = 0.0;
+ double mOutputPhase = 0.0;
+ double mOutputAmplitude = 0.75;
+ // If this jumps around then we are probably just hearing noise.
+ double mPhaseOffset = 0.0;
+ double mMagnitude = 0.0;
+ int32_t mFramesAccumulated = 0;
+ double mSinAccumulator = 0.0;
+ double mCosAccumulator = 0.0;
+ double mScaledTolerance = 0.0;
+
+ InfiniteRecording<float> mInfiniteRecording;
+
+private:
+ int32_t mInputChannel = 0;
+ int32_t mOutputChannel = 0;
+ float mTolerance = 0.10; // scaled from 0.0 to 1.0
+
+ float mNoiseAmplitude = 0.00; // Used to experiment with warbling caused by DRC.
+ PseudoRandom mWhiteNoise;
+};
+
+#endif //ANALYZER_BASE_SINE_ANALYZER_H
diff --git a/apps/OboeTester/app/src/main/cpp/analyzer/DataPathAnalyzer.h b/apps/OboeTester/app/src/main/cpp/analyzer/DataPathAnalyzer.h
new file mode 100644
index 00000000..31369b4d
--- /dev/null
+++ b/apps/OboeTester/app/src/main/cpp/analyzer/DataPathAnalyzer.h
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2020 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 ANALYZER_DATA_PATH_ANALYZER_H
+#define ANALYZER_DATA_PATH_ANALYZER_H
+
+#include <algorithm>
+#include <cctype>
+#include <iomanip>
+#include <iostream>
+#include <math.h>
+
+#include "BaseSineAnalyzer.h"
+#include "InfiniteRecording.h"
+#include "LatencyAnalyzer.h"
+
+/**
+ * Output a steady sine wave and analyze the return signal.
+ *
+ * Use a cosine transform to measure the predicted magnitude and relative phase of the
+ * looped back sine wave.
+ */
+class DataPathAnalyzer : public BaseSineAnalyzer {
+public:
+
+ DataPathAnalyzer() : BaseSineAnalyzer() {
+ // Add a little bit of noise to reduce blockage by speaker protection and DRC.
+ setNoiseAmplitude(0.05);
+ }
+
+ /**
+ * @param frameData contains microphone data with sine signal feedback
+ * @param channelCount
+ */
+ result_code processInputFrame(float *frameData, int /* channelCount */) override {
+ result_code result = RESULT_OK;
+
+ float sample = frameData[getInputChannel()];
+ mInfiniteRecording.write(sample);
+
+ if (transformSample(sample, mOutputPhase)) {
+ resetAccumulator();
+ }
+
+ // Update MaxMagnitude if we are locked.
+ double diff = abs(mPhaseOffset - mPreviousPhaseOffset);
+ if (diff < mPhaseTolerance) {
+ mMaxMagnitude = std::max(mMagnitude, mMaxMagnitude);
+ }
+ mPreviousPhaseOffset = mPhaseOffset;
+ return result;
+ }
+
+ std::string analyze() override {
+ std::stringstream report;
+ report << "DataPathAnalyzer ------------------\n";
+ report << LOOPBACK_RESULT_TAG "sine.magnitude = " << std::setw(8)
+ << mMagnitude << "\n";
+ report << LOOPBACK_RESULT_TAG "frames.accumulated = " << std::setw(8)
+ << mFramesAccumulated << "\n";
+ report << LOOPBACK_RESULT_TAG "sine.period = " << std::setw(8)
+ << mSinePeriod << "\n";
+ return report.str();
+ }
+
+ void reset() override {
+ BaseSineAnalyzer::reset();
+ mPreviousPhaseOffset = 999.0; // Arbitrary high offset to prevent early lock.
+ mMaxMagnitude = 0.0;
+ }
+
+ double getMaxMagnitude() {
+ return mMaxMagnitude;
+ }
+
+private:
+ double mPreviousPhaseOffset = 0.0;
+ double mPhaseTolerance = 2 * M_PI / 48;
+ double mMaxMagnitude = 0.0;
+};
+#endif // ANALYZER_DATA_PATH_ANALYZER_H
diff --git a/apps/OboeTester/app/src/main/cpp/analyzer/GlitchAnalyzer.h b/apps/OboeTester/app/src/main/cpp/analyzer/GlitchAnalyzer.h
index 4d1bd6f0..249bdd1a 100644
--- a/apps/OboeTester/app/src/main/cpp/analyzer/GlitchAnalyzer.h
+++ b/apps/OboeTester/app/src/main/cpp/analyzer/GlitchAnalyzer.h
@@ -24,6 +24,7 @@
#include "InfiniteRecording.h"
#include "LatencyAnalyzer.h"
+#include "BaseSineAnalyzer.h"
#include "PseudoRandom.h"
/**
@@ -32,12 +33,10 @@
* Use a cosine transform to measure the predicted magnitude and relative phase of the
* looped back sine wave. Then generate a predicted signal and compare with the actual signal.
*/
-class GlitchAnalyzer : public LoopbackProcessor {
+class GlitchAnalyzer : public BaseSineAnalyzer {
public:
- GlitchAnalyzer()
- : LoopbackProcessor()
- , mInfiniteRecording(64 * 1024) {}
+ GlitchAnalyzer() : BaseSineAnalyzer() {}
int32_t getState() const {
return mState;
@@ -47,20 +46,6 @@ public:
return mPeakFollower.getLevel();
}
- double getTolerance() {
- return mTolerance;
- }
-
- void setTolerance(double tolerance) {
- mTolerance = tolerance;
- mScaledTolerance = mMagnitude * mTolerance;
- }
-
- void setMagnitude(double magnitude) {
- mMagnitude = magnitude;
- mScaledTolerance = mMagnitude * mTolerance;
- }
-
int32_t getGlitchCount() const {
return mGlitchCount;
}
@@ -126,26 +111,6 @@ public:
void printStatus() override {
ALOGD("st = %d, #gl = %3d,", mState, mGlitchCount);
}
- /**
- * Calculate the magnitude of the component of the input signal
- * that matches the analysis frequency.
- * Also calculate the phase that we can use to create a
- * signal that matches that component.
- * The phase will be between -PI and +PI.
- */
- double calculateMagnitude(double *phasePtr = nullptr) {
- if (mFramesAccumulated == 0) {
- return 0.0;
- }
- double sinMean = mSinAccumulator / mFramesAccumulated;
- double cosMean = mCosAccumulator / mFramesAccumulated;
- double magnitude = 2.0 * sqrt((sinMean * sinMean) + (cosMean * cosMean));
- if (phasePtr != nullptr) {
- double phase = M_PI_2 - atan2(sinMean, cosMean);
- *phasePtr = phase;
- }
- return magnitude;
- }
/**
* @param frameData contains microphone data with sine signal feedback
@@ -204,17 +169,16 @@ public:
mFramesAccumulated++;
// Must be a multiple of the period or the calculation will not be accurate.
if (mFramesAccumulated == mSinePeriod * PERIODS_NEEDED_FOR_LOCK) {
- double phaseOffset = 0.0;
- setMagnitude(calculateMagnitude(&phaseOffset));
+ setMagnitude(calculateMagnitudePhase(&mPhaseOffset));
// ALOGD("%s() mag = %f, offset = %f, prev = %f",
// __func__, mMagnitude, mPhaseOffset, mPreviousPhaseOffset);
if (mMagnitude > mThreshold) {
- if (abs(phaseOffset) < kMaxPhaseError) {
+ if (abs(mPhaseOffset) < kMaxPhaseError) {
mState = STATE_LOCKED;
// ALOGD("%5d: switch to STATE_LOCKED", mFrameCounter);
}
// Adjust mInputPhase to match measured phase
- mInputPhase += phaseOffset;
+ mInputPhase += mPhaseOffset;
}
resetAccumulator();
}
@@ -234,27 +198,20 @@ public:
} else {
mSumSquareSignal += predicted * predicted;
mSumSquareNoise += diff * diff;
+
+
// Track incoming signal and slowly adjust magnitude to account
// for drift in the DRC or AGC.
- mSinAccumulator += sample * sinf(mInputPhase);
- mCosAccumulator += sample * cosf(mInputPhase);
- mFramesAccumulated++;
// Must be a multiple of the period or the calculation will not be accurate.
- if (mFramesAccumulated == mSinePeriod) {
- const double coefficient = 0.1;
- double phaseOffset = 0.0;
- double magnitude = calculateMagnitude(&phaseOffset);
- // One pole averaging filter.
- setMagnitude((mMagnitude * (1.0 - coefficient)) + (magnitude * coefficient));
-
+ if (transformSample(sample, mInputPhase)) {
mMeanSquareNoise = mSumSquareNoise * mInverseSinePeriod;
mMeanSquareSignal = mSumSquareSignal * mInverseSinePeriod;
resetAccumulator();
- if (abs(phaseOffset) > kMaxPhaseError) {
+ if (abs(mPhaseOffset) > kMaxPhaseError) {
result = ERROR_GLITCHES;
onGlitchStart();
- ALOGD("phase glitch detected, phaseOffset = %g", phaseOffset);
+ ALOGD("phase glitch detected, phaseOffset = %g", mPhaseOffset);
} else if (mMagnitude < mThreshold) {
result = ERROR_GLITCHES;
onGlitchStart();
@@ -304,34 +261,7 @@ public:
}
}
- // advance and wrap phase
- void incrementOutputPhase() {
- mOutputPhase += mPhaseIncrement;
- if (mOutputPhase > M_PI) {
- mOutputPhase -= (2.0 * M_PI);
- }
- }
-
- /**
- * @param frameData upon return, contains the reference sine wave
- * @param channelCount
- */
- result_code processOutputFrame(float *frameData, int channelCount) override {
- float output = 0.0f;
- // Output sine wave so we can measure it.
- if (mState != STATE_IDLE) {
- float sinOut = sinf(mOutputPhase);
- incrementOutputPhase();
- output = (sinOut * mOutputAmplitude)
- + (mWhiteNoise.nextRandomDouble() * kNoiseAmplitude);
- // ALOGD("sin(%f) = %f, %f\n", mOutputPhase, sinOut, mPhaseIncrement);
- }
- frameData[0] = output;
- for (int i = 1; i < channelCount; i++) {
- frameData[i] = 0.0f;
- }
- return RESULT_OK;
- }
+ bool isOutputEnabled() override { return mState != STATE_IDLE; }
void onGlitchStart() {
mGlitchCount++;
@@ -349,10 +279,8 @@ public:
}
// reset the sine wave detector
- void resetAccumulator() {
- mFramesAccumulated = 0;
- mSinAccumulator = 0.0;
- mCosAccumulator = 0.0;
+ void resetAccumulator() override {
+ BaseSineAnalyzer::resetAccumulator();
mSumSquareSignal = 0.0;
mSumSquareNoise = 0.0;
}
@@ -364,18 +292,13 @@ public:
}
void reset() override {
- LoopbackProcessor::reset();
+ BaseSineAnalyzer::reset();
mState = STATE_IDLE;
mDownCounter = IDLE_FRAME_COUNT;
- resetAccumulator();
}
void prepareToTest() override {
- LoopbackProcessor::prepareToTest();
- mSinePeriod = getSampleRate() / kTargetGlitchFrequency;
- mOutputPhase = 0.0f;
- mInverseSinePeriod = 1.0 / mSinePeriod;
- mPhaseIncrement = 2.0 * M_PI * mInverseSinePeriod;
+ BaseSineAnalyzer::prepareToTest();
mGlitchCount = 0;
mMaxGlitchDelta = 0.0;
for (int i = 0; i < NUM_STATES; i++) {
@@ -408,33 +331,21 @@ private:
MIN_SNR_DB = 65
};
- static constexpr float kNoiseAmplitude = 0.00; // Used to experiment with warbling caused by DRC.
- static constexpr int kTargetGlitchFrequency = 607;
static constexpr double kMaxPhaseError = M_PI * 0.05;
- float mTolerance = 0.10; // scaled from 0.0 to 1.0
double mThreshold = 0.005;
- int mSinePeriod = 1; // this will be set before use
- double mInverseSinePeriod = 1.0;
int32_t mStateFrameCounters[NUM_STATES];
+ sine_state_t mState = STATE_IDLE;
+ int64_t mLastGlitchPosition;
- double mPhaseIncrement = 0.0;
double mInputPhase = 0.0;
- double mOutputPhase = 0.0;
- double mMagnitude = 0.0;
- int32_t mFramesAccumulated = 0;
- double mSinAccumulator = 0.0;
- double mCosAccumulator = 0.0;
double mMaxGlitchDelta = 0.0;
int32_t mGlitchCount = 0;
int32_t mNonGlitchCount = 0;
int32_t mGlitchLength = 0;
- // This is used for processing every frame so we cache it here.
- double mScaledTolerance = 0.0;
int mDownCounter = IDLE_FRAME_COUNT;
int32_t mFrameCounter = 0;
- double mOutputAmplitude = 0.75;
int32_t mForceGlitchDuration = 0; // if > 0 then force a glitch for debugging
int32_t mForceGlitchCounter = 4 * 48000; // count down and trigger at zero
@@ -446,13 +357,6 @@ private:
double mMeanSquareNoise = 0.0;
PeakDetector mPeakFollower;
-
- PseudoRandom mWhiteNoise;
-
- sine_state_t mState = STATE_IDLE;
-
- InfiniteRecording<float> mInfiniteRecording;
- int64_t mLastGlitchPosition;
};
diff --git a/apps/OboeTester/app/src/main/cpp/analyzer/LatencyAnalyzer.h b/apps/OboeTester/app/src/main/cpp/analyzer/LatencyAnalyzer.h
index 3178c6e0..d78e4855 100644
--- a/apps/OboeTester/app/src/main/cpp/analyzer/LatencyAnalyzer.h
+++ b/apps/OboeTester/app/src/main/cpp/analyzer/LatencyAnalyzer.h
@@ -49,8 +49,8 @@
#define LOOPBACK_RESULT_TAG "RESULT: "
static constexpr int32_t kDefaultSampleRate = 48000;
-static constexpr int32_t kMillisPerSecond = 1000;
-static constexpr int32_t kMaxLatencyMillis = 700; // arbitrary and generous
+static constexpr int32_t kMillisPerSecond = 1000; // by definition
+static constexpr int32_t kMaxLatencyMillis = 1000; // arbitrary and generous
static constexpr double kMinimumConfidence = 0.2;
struct LatencyReport {
diff --git a/apps/OboeTester/app/src/main/cpp/jni-bridge.cpp b/apps/OboeTester/app/src/main/cpp/jni-bridge.cpp
index 09c273fd..39dba112 100644
--- a/apps/OboeTester/app/src/main/cpp/jni-bridge.cpp
+++ b/apps/OboeTester/app/src/main/cpp/jni-bridge.cpp
@@ -89,12 +89,12 @@ Java_com_google_sample_oboe_manualtest_OboeAudioOutputStream_setAmplitude(JNIEnv
JNIEXPORT jboolean JNICALL
Java_com_google_sample_oboe_manualtest_NativeEngine_isMMapSupported(JNIEnv *env, jclass type) {
- return AAudioExtensions::getInstance().isMMapSupported();
+ return oboe::AAudioExtensions::getInstance().isMMapSupported();
}
JNIEXPORT jboolean JNICALL
Java_com_google_sample_oboe_manualtest_NativeEngine_isMMapExclusiveSupported(JNIEnv *env, jclass type) {
- return AAudioExtensions::getInstance().isMMapExclusiveSupported();
+ return oboe::AAudioExtensions::getInstance().isMMapExclusiveSupported();
}
JNIEXPORT void JNICALL
@@ -179,7 +179,7 @@ Java_com_google_sample_oboe_manualtest_OboeAudioStream_close(JNIEnv *env, jobjec
JNIEXPORT jint JNICALL
Java_com_google_sample_oboe_manualtest_OboeAudioStream_setBufferSizeInFrames(
JNIEnv *env, jobject, jint streamIndex, jint threshold) {
- oboe::AudioStream *oboeStream = engine.getCurrentActivity()->getStream(streamIndex);
+ std::shared_ptr<oboe::AudioStream> oboeStream = engine.getCurrentActivity()->getStream(streamIndex);
if (oboeStream != nullptr) {
auto result = oboeStream->setBufferSizeInFrames(threshold);
return (!result)
@@ -193,7 +193,7 @@ JNIEXPORT jint JNICALL
Java_com_google_sample_oboe_manualtest_OboeAudioStream_getBufferSizeInFrames(
JNIEnv *env, jobject, jint streamIndex) {
jint result = (jint) oboe::Result::ErrorNull;
- oboe::AudioStream *oboeStream = engine.getCurrentActivity()->getStream(streamIndex);
+ std::shared_ptr<oboe::AudioStream> oboeStream = engine.getCurrentActivity()->getStream(streamIndex);
if (oboeStream != nullptr) {
result = oboeStream->getBufferSizeInFrames();
}
@@ -204,7 +204,7 @@ JNIEXPORT jint JNICALL
Java_com_google_sample_oboe_manualtest_OboeAudioStream_getBufferCapacityInFrames(
JNIEnv *env, jobject, jint streamIndex) {
jint result = (jint) oboe::Result::ErrorNull;
- oboe::AudioStream *oboeStream = engine.getCurrentActivity()->getStream(streamIndex);
+ std::shared_ptr<oboe::AudioStream> oboeStream = engine.getCurrentActivity()->getStream(streamIndex);
if (oboeStream != nullptr) {
result = oboeStream->getBufferCapacityInFrames();
}
@@ -228,7 +228,7 @@ JNIEXPORT jint JNICALL
Java_com_google_sample_oboe_manualtest_OboeAudioStream_getNativeApi(
JNIEnv *env, jobject, jint streamIndex) {
jint result = (jint) oboe::Result::ErrorNull;
- oboe::AudioStream *oboeStream = engine.getCurrentActivity()->getStream(streamIndex);
+ std::shared_ptr<oboe::AudioStream> oboeStream = engine.getCurrentActivity()->getStream(streamIndex);
if (oboeStream != nullptr) {
oboe::AudioApi audioApi = oboeStream->getAudioApi();
result = convertAudioApiToNativeApi(audioApi);
@@ -241,7 +241,7 @@ JNIEXPORT jint JNICALL
Java_com_google_sample_oboe_manualtest_OboeAudioStream_getSampleRate(
JNIEnv *env, jobject, jint streamIndex) {
jint result = (jint) oboe::Result::ErrorNull;
- oboe::AudioStream *oboeStream = engine.getCurrentActivity()->getStream(streamIndex);
+ std::shared_ptr<oboe::AudioStream> oboeStream = engine.getCurrentActivity()->getStream(streamIndex);
if (oboeStream != nullptr) {
result = oboeStream->getSampleRate();
}
@@ -252,7 +252,7 @@ JNIEXPORT jint JNICALL
Java_com_google_sample_oboe_manualtest_OboeAudioStream_getSharingMode(
JNIEnv *env, jobject, jint streamIndex) {
jint result = (jint) oboe::Result::ErrorNull;
- oboe::AudioStream *oboeStream = engine.getCurrentActivity()->getStream(streamIndex);
+ std::shared_ptr<oboe::AudioStream> oboeStream = engine.getCurrentActivity()->getStream(streamIndex);
if (oboeStream != nullptr) {
result = (jint) oboeStream->getSharingMode();
}
@@ -263,7 +263,7 @@ JNIEXPORT jint JNICALL
Java_com_google_sample_oboe_manualtest_OboeAudioStream_getPerformanceMode(
JNIEnv *env, jobject, jint streamIndex) {
jint result = (jint) oboe::Result::ErrorNull;
- oboe::AudioStream *oboeStream = engine.getCurrentActivity()->getStream(streamIndex);
+ std::shared_ptr<oboe::AudioStream> oboeStream = engine.getCurrentActivity()->getStream(streamIndex);
if (oboeStream != nullptr) {
result = (jint) oboeStream->getPerformanceMode();
}
@@ -274,7 +274,7 @@ JNIEXPORT jint JNICALL
Java_com_google_sample_oboe_manualtest_OboeAudioStream_getInputPreset(
JNIEnv *env, jobject, jint streamIndex) {
jint result = (jint) oboe::Result::ErrorNull;
- oboe::AudioStream *oboeStream = engine.getCurrentActivity()->getStream(streamIndex);
+ std::shared_ptr<oboe::AudioStream> oboeStream = engine.getCurrentActivity()->getStream(streamIndex);
if (oboeStream != nullptr) {
result = (jint) oboeStream->getInputPreset();
}
@@ -285,7 +285,7 @@ JNIEXPORT jint JNICALL
Java_com_google_sample_oboe_manualtest_OboeAudioStream_getFramesPerBurst(
JNIEnv *env, jobject, jint streamIndex) {
jint result = (jint) oboe::Result::ErrorNull;
- oboe::AudioStream *oboeStream = engine.getCurrentActivity()->getStream(streamIndex);
+ std::shared_ptr<oboe::AudioStream> oboeStream = engine.getCurrentActivity()->getStream(streamIndex);
if (oboeStream != nullptr) {
result = oboeStream->getFramesPerBurst();
}
@@ -296,7 +296,7 @@ JNIEXPORT jint JNICALL
Java_com_google_sample_oboe_manualtest_OboeAudioStream_getChannelCount(
JNIEnv *env, jobject, jint streamIndex) {
jint result = (jint) oboe::Result::ErrorNull;
- oboe::AudioStream *oboeStream = engine.getCurrentActivity()->getStream(streamIndex);
+ std::shared_ptr<oboe::AudioStream> oboeStream = engine.getCurrentActivity()->getStream(streamIndex);
if (oboeStream != nullptr) {
result = oboeStream->getChannelCount();
}
@@ -306,7 +306,7 @@ Java_com_google_sample_oboe_manualtest_OboeAudioStream_getChannelCount(
JNIEXPORT jint JNICALL
Java_com_google_sample_oboe_manualtest_OboeAudioStream_getFormat(JNIEnv *env, jobject instance, jint streamIndex) {
jint result = (jint) oboe::Result::ErrorNull;
- oboe::AudioStream *oboeStream = engine.getCurrentActivity()->getStream(streamIndex);
+ std::shared_ptr<oboe::AudioStream> oboeStream = engine.getCurrentActivity()->getStream(streamIndex);
if (oboeStream != nullptr) {
result = (jint) oboeStream->getFormat();
}
@@ -317,7 +317,7 @@ JNIEXPORT jint JNICALL
Java_com_google_sample_oboe_manualtest_OboeAudioStream_getDeviceId(
JNIEnv *env, jobject, jint streamIndex) {
jint result = (jint) oboe::Result::ErrorNull;
- oboe::AudioStream *oboeStream = engine.getCurrentActivity()->getStream(streamIndex);
+ std::shared_ptr<oboe::AudioStream> oboeStream = engine.getCurrentActivity()->getStream(streamIndex);
if (oboeStream != nullptr) {
result = oboeStream->getDeviceId();
}
@@ -328,7 +328,7 @@ JNIEXPORT jint JNICALL
Java_com_google_sample_oboe_manualtest_OboeAudioStream_getSessionId(
JNIEnv *env, jobject, jint streamIndex) {
jint result = (jint) oboe::Result::ErrorNull;
- oboe::AudioStream *oboeStream = engine.getCurrentActivity()->getStream(streamIndex);
+ std::shared_ptr<oboe::AudioStream> oboeStream = engine.getCurrentActivity()->getStream(streamIndex);
if (oboeStream != nullptr) {
result = oboeStream->getSessionId();
}
@@ -339,7 +339,7 @@ JNIEXPORT jlong JNICALL
Java_com_google_sample_oboe_manualtest_OboeAudioStream_getFramesWritten(
JNIEnv *env, jobject, jint streamIndex) {
jlong result = (jint) oboe::Result::ErrorNull;
- oboe::AudioStream *oboeStream = engine.getCurrentActivity()->getStream(streamIndex);
+ std::shared_ptr<oboe::AudioStream> oboeStream = engine.getCurrentActivity()->getStream(streamIndex);
if (oboeStream != nullptr) {
result = oboeStream->getFramesWritten();
}
@@ -350,7 +350,7 @@ JNIEXPORT jlong JNICALL
Java_com_google_sample_oboe_manualtest_OboeAudioStream_getFramesRead(
JNIEnv *env, jobject, jint streamIndex) {
jlong result = (jlong) oboe::Result::ErrorNull;
- oboe::AudioStream *oboeStream = engine.getCurrentActivity()->getStream(streamIndex);
+ std::shared_ptr<oboe::AudioStream> oboeStream = engine.getCurrentActivity()->getStream(streamIndex);
if (oboeStream != nullptr) {
result = oboeStream->getFramesRead();
}
@@ -361,7 +361,7 @@ JNIEXPORT jint JNICALL
Java_com_google_sample_oboe_manualtest_OboeAudioStream_getXRunCount(
JNIEnv *env, jobject, jint streamIndex) {
jint result = (jlong) oboe::Result::ErrorNull;
- oboe::AudioStream *oboeStream = engine.getCurrentActivity()->getStream(streamIndex);
+ std::shared_ptr<oboe::AudioStream> oboeStream = engine.getCurrentActivity()->getStream(streamIndex);
if (oboeStream != nullptr) {
auto oboeResult = oboeStream->getXRunCount();
if (!oboeResult) {
@@ -382,7 +382,7 @@ Java_com_google_sample_oboe_manualtest_OboeAudioStream_getCallbackCount(
JNIEXPORT jint JNICALL
Java_com_google_sample_oboe_manualtest_OboeAudioStream_getLastErrorCallbackResult(
JNIEnv *env, jobject, jint streamIndex) {
- oboe::AudioStream *oboeStream = engine.getCurrentActivity()->getStream(streamIndex);
+ std::shared_ptr<oboe::AudioStream> oboeStream = engine.getCurrentActivity()->getStream(streamIndex);
if (oboeStream != nullptr) {
return (jint) oboeStream->getLastErrorCallbackResult();
}
@@ -390,14 +390,10 @@ Java_com_google_sample_oboe_manualtest_OboeAudioStream_getLastErrorCallbackResul
}
JNIEXPORT jdouble JNICALL
-Java_com_google_sample_oboe_manualtest_OboeAudioStream_getLatency(JNIEnv *env,
- jobject instance, jint streamIndex) {
- oboe::AudioStream *oboeStream = engine.getCurrentActivity()->getStream(streamIndex);
- if (oboeStream != nullptr) {
- auto result = oboeStream->calculateLatencyMillis();
- return (!result) ? -1.0 : result.value();
- }
- return -1.0;
+Java_com_google_sample_oboe_manualtest_OboeAudioStream_getTimestampLatency(JNIEnv *env,
+ jobject instance,
+ jint streamIndex) {
+ return engine.getCurrentActivity()->getTimestampLatency(streamIndex);
}
JNIEXPORT jdouble JNICALL
@@ -413,7 +409,7 @@ Java_com_google_sample_oboe_manualtest_OboeAudioStream_setWorkload(
JNIEXPORT jint JNICALL
Java_com_google_sample_oboe_manualtest_OboeAudioStream_getState(JNIEnv *env, jobject instance, jint streamIndex) {
- oboe::AudioStream *oboeStream = engine.getCurrentActivity()->getStream(streamIndex);
+ std::shared_ptr<oboe::AudioStream> oboeStream = engine.getCurrentActivity()->getStream(streamIndex);
if (oboeStream != nullptr) {
auto state = oboeStream->getState();
if (state != oboe::StreamState::Starting && state != oboe::StreamState::Started) {
@@ -528,6 +524,18 @@ Java_com_google_sample_oboe_manualtest_EchoActivity_setDelayTime(JNIEnv *env,
engine.setDelayTime(delayTimeSeconds);
}
+JNIEXPORT int JNICALL
+Java_com_google_sample_oboe_manualtest_EchoActivity_getColdStartInputMillis(JNIEnv *env,
+ jobject instance) {
+ return engine.getCurrentActivity()->getColdStartInputMillis();
+}
+
+JNIEXPORT int JNICALL
+Java_com_google_sample_oboe_manualtest_EchoActivity_getColdStartOutputMillis(JNIEnv *env,
+ jobject instance) {
+ return engine.getCurrentActivity()->getColdStartOutputMillis();
+}
+
// ==========================================================================
JNIEXPORT jint JNICALL
Java_com_google_sample_oboe_manualtest_RoundTripLatencyActivity_getAnalyzerProgress(JNIEnv *env,
@@ -603,12 +611,31 @@ Java_com_google_sample_oboe_manualtest_GlitchActivity_getSignalToNoiseDB(JNIEnv
jobject instance) {
return engine.mActivityGlitches.getGlitchAnalyzer()->getSignalToNoiseDB();
}
+
JNIEXPORT jdouble JNICALL
Java_com_google_sample_oboe_manualtest_GlitchActivity_getPeakAmplitude(JNIEnv *env,
jobject instance) {
return engine.mActivityGlitches.getGlitchAnalyzer()->getPeakAmplitude();
}
+JNIEXPORT jdouble JNICALL
+Java_com_google_sample_oboe_manualtest_TestDataPathsActivity_getMagnitude(JNIEnv *env,
+ jobject instance) {
+ return engine.mActivityDataPath.getDataPathAnalyzer()->getMagnitude();
+}
+
+JNIEXPORT jdouble JNICALL
+Java_com_google_sample_oboe_manualtest_TestDataPathsActivity_getMaxMagnitude(JNIEnv *env,
+ jobject instance) {
+ return engine.mActivityDataPath.getDataPathAnalyzer()->getMaxMagnitude();
+}
+
+JNIEXPORT jdouble JNICALL
+Java_com_google_sample_oboe_manualtest_TestDataPathsActivity_getPhase(JNIEnv *env,
+ jobject instance) {
+ return engine.mActivityDataPath.getDataPathAnalyzer()->getPhaseOffset();
+}
+
JNIEXPORT void JNICALL
Java_com_google_sample_oboe_manualtest_GlitchActivity_setTolerance(JNIEnv *env,
jobject instance,
@@ -618,6 +645,30 @@ Java_com_google_sample_oboe_manualtest_GlitchActivity_setTolerance(JNIEnv *env,
}
}
+JNIEXPORT void JNICALL
+Java_com_google_sample_oboe_manualtest_GlitchActivity_setInputChannelNative(JNIEnv *env,
+ jobject instance,
+ jint channel) {
+ if (engine.mActivityGlitches.getGlitchAnalyzer()) {
+ engine.mActivityGlitches.getGlitchAnalyzer()->setInputChannel(channel);
+ }
+ if (engine.mActivityDataPath.getDataPathAnalyzer()) {
+ engine.mActivityDataPath.getDataPathAnalyzer()->setInputChannel(channel);
+ }
+}
+
+JNIEXPORT void JNICALL
+Java_com_google_sample_oboe_manualtest_GlitchActivity_setOutputChannelNative(JNIEnv *env,
+ jobject instance,
+ jint channel) {
+ if (engine.mActivityGlitches.getGlitchAnalyzer()) {
+ engine.mActivityGlitches.getGlitchAnalyzer()->setOutputChannel(channel);
+ }
+ if (engine.mActivityDataPath.getDataPathAnalyzer()) {
+ engine.mActivityDataPath.getDataPathAnalyzer()->setOutputChannel(channel);
+ }
+}
+
JNIEXPORT jint JNICALL
Java_com_google_sample_oboe_manualtest_ManualGlitchActivity_getGlitch(JNIEnv *env, jobject instance,
jfloatArray waveform_) {
diff --git a/apps/OboeTester/app/src/main/java/com/google/sample/audio_device/AudioDeviceInfoConverter.java b/apps/OboeTester/app/src/main/java/com/google/sample/audio_device/AudioDeviceInfoConverter.java
index f736667f..b1419b36 100644
--- a/apps/OboeTester/app/src/main/java/com/google/sample/audio_device/AudioDeviceInfoConverter.java
+++ b/apps/OboeTester/app/src/main/java/com/google/sample/audio_device/AudioDeviceInfoConverter.java
@@ -97,11 +97,13 @@ public class AudioDeviceInfoConverter {
case AudioDeviceInfo.TYPE_BLUETOOTH_SCO:
return "Bluetooth telephony SCO";
case AudioDeviceInfo.TYPE_BUILTIN_EARPIECE:
- return "built-in earphone speaker";
+ return "built-in earpiece";
case AudioDeviceInfo.TYPE_BUILTIN_MIC:
return "built-in microphone";
case AudioDeviceInfo.TYPE_BUILTIN_SPEAKER:
return "built-in speaker";
+ case 0x18: // AudioDeviceInfo.TYPE_BUILTIN_SPEAKER_SAFE:
+ return "built-in speaker safe";
case AudioDeviceInfo.TYPE_BUS:
return "BUS";
case AudioDeviceInfo.TYPE_DOCK:
@@ -134,7 +136,7 @@ public class AudioDeviceInfoConverter {
return "wired headset";
default:
case AudioDeviceInfo.TYPE_UNKNOWN:
- return "unknown";
+ return "unknown=" + type;
}
}
}
diff --git a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/AnalyzerActivity.java b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/AnalyzerActivity.java
index 443a2584..a2872867 100644
--- a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/AnalyzerActivity.java
+++ b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/AnalyzerActivity.java
@@ -143,9 +143,6 @@ public class AnalyzerActivity extends TestInputActivity {
super.onCreate(savedInstanceState);
mAudioOutTester = addAudioOutputTester();
mBufferSizeView = (BufferSizeView) findViewById(R.id.buffer_size_view);
- if (mBufferSizeView != null) {
- mBufferSizeView.setAudioOutTester(mAudioOutTester);
- }
}
@Override
@@ -169,11 +166,12 @@ public class AnalyzerActivity extends TestInputActivity {
}
}
- public void startAudio() {
- if (mBufferSizeView != null && mBufferSizeView.isEnabled()) {
- mBufferSizeView.updateBufferSize();
+ @Override
+ public void openAudio() throws IOException {
+ super.openAudio();
+ if (mBufferSizeView != null) {
+ mBufferSizeView.onStreamOpened((OboeAudioStream) mAudioOutTester.getCurrentAudioStream());
}
- super.startAudio();
}
public void onStreamClosed() {
diff --git a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/AudioStreamBase.java b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/AudioStreamBase.java
index c2efc6a5..697c28b1 100644
--- a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/AudioStreamBase.java
+++ b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/AudioStreamBase.java
@@ -25,6 +25,7 @@ public abstract class AudioStreamBase {
private StreamConfiguration mRequestedStreamConfiguration;
private StreamConfiguration mActualStreamConfiguration;
+ AudioStreamBase.DoubleStatistics mLatencyStatistics;
private int mBufferSizeInFrames;
@@ -36,11 +37,40 @@ public abstract class AudioStreamBase {
status.framesWritten = getFramesWritten();
status.callbackCount = getCallbackCount();
status.latency = getLatency();
+ mLatencyStatistics.add(status.latency);
status.cpuLoad = getCpuLoad();
status.state = getState();
return status;
}
+ public DoubleStatistics getLatencyStatistics() {
+ return mLatencyStatistics;
+ }
+
+ public static class DoubleStatistics {
+ private double sum;
+ private int count;
+ private double minimum = Double.MAX_VALUE;
+ private double maximum = Double.MIN_VALUE;
+
+ void add(double latency) {
+ if (latency <= 0.0) return;
+ sum += latency;
+ count++;
+ minimum = Math.min(latency, minimum);
+ maximum = Math.max(latency, maximum);
+ }
+
+ double getAverage() {
+ return sum / count;
+ }
+
+ public String dump() {
+ if (count == 0) return "?";
+ return String.format("%3.1f/%3.1f/%3.1f ms", minimum, getAverage(), maximum);
+ }
+ }
+
/**
* Changes dynamic at run-time.
*/
@@ -65,15 +95,11 @@ public abstract class AudioStreamBase {
buffer.append("frames written " + framesWritten + " - read " + framesRead
+ " = " + (framesWritten - framesRead) + "\n");
- String latencyText = (latency < 0.0)
- ? "?"
- : String.format("%6.1f ms", latency);
String cpuLoadText = String.format("%2d%c", (int)(cpuLoad * 100), '%');
buffer.append(
convertStateToString(state)
+ ", #cb=" + callbackCount
+ ", f/cb=" + String.format("%3d", framesPerCallback)
- + ", latnc = " + latencyText
+ ", " + cpuLoadText + " cpu"
+ "\n");
@@ -116,6 +142,7 @@ public abstract class AudioStreamBase {
mRequestedStreamConfiguration = requestedConfiguration;
mActualStreamConfiguration = actualConfiguration;
mBufferSizeInFrames = bufferSizeInFrames;
+ mLatencyStatistics = new AudioStreamBase.DoubleStatistics();
}
public abstract boolean isInput();
@@ -174,5 +201,4 @@ public abstract class AudioStreamBase {
public abstract int getXRunCount();
-
}
diff --git a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/AutoGlitchActivity.java b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/AutoGlitchActivity.java
deleted file mode 100644
index f86368be..00000000
--- a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/AutoGlitchActivity.java
+++ /dev/null
@@ -1,330 +0,0 @@
-/*
- * Copyright 2019 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.
- */
-
-package com.google.sample.oboe.manualtest;
-
-import android.content.Intent;
-import android.os.Build;
-import android.os.Bundle;
-import android.text.method.ScrollingMovementMethod;
-import android.view.View;
-import android.widget.AdapterView;
-import android.widget.Spinner;
-import android.widget.TextView;
-
-import java.io.IOException;
-import java.util.Date;
-
-public class AutoGlitchActivity extends GlitchActivity implements Runnable {
-
- private static final int SETUP_TIME_SECONDS = 4; // Time for the stream to settle.
- private static final int DEFAULT_DURATION_SECONDS = 8; // Run time for each test.
- private static final int DEFAULT_GAP_MILLIS = 400; // Run time for each test.
- private static final String TEXT_SKIP = "SKIP";
- public static final String TEXT_PASS = "PASS";
- public static final String TEXT_FAIL = "FAIL !!!!";
-
- private TextView mAutoTextView;
-
- private Thread mAutoThread;
- private volatile boolean mThreadEnabled = false;
- int mTestCount = 0;
- private int mDurationSeconds = DEFAULT_DURATION_SECONDS;
- private int mGapMillis = DEFAULT_GAP_MILLIS;
- private StringBuffer mFailedSummary;
- private int mPassCount = 0;
- private int mFailCount = 0;
- private Spinner mDurationSpinner;
-
- // Test with these configurations.
- private static final int[] PERFORMANCE_MODES = {
- StreamConfiguration.PERFORMANCE_MODE_LOW_LATENCY,
- StreamConfiguration.PERFORMANCE_MODE_NONE
- };
- private static final int[] SAMPLE_RATES = { 48000, 44100, 16000 };
-
- private class DurationSpinnerListener implements android.widget.AdapterView.OnItemSelectedListener {
- @Override
- public void onItemSelected(AdapterView<?> parent, View view, int pos, long id) {
- String text = parent.getItemAtPosition(pos).toString();
- mDurationSeconds = Integer.parseInt(text);
- }
-
- @Override
- public void onNothingSelected(AdapterView<?> parent) {
- mDurationSeconds = DEFAULT_DURATION_SECONDS;
- }
- }
-
- @Override
- protected void inflateActivity() {
- setContentView(R.layout.activity_auto_glitches);
- }
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- mAutoTextView = (TextView) findViewById(R.id.text_log);
- mAutoTextView.setMovementMethod(new ScrollingMovementMethod());
-
- mDurationSpinner = (Spinner) findViewById(R.id.spinner_glitch_duration);
- mDurationSpinner.setOnItemSelectedListener(new DurationSpinnerListener());
- }
-
- // Write to scrollable TextView
- private void log(final String text) {
- runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mAutoTextView.append(text);
- mAutoTextView.append("\n");
- }
- });
- }
- private void logClear() {
- runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mAutoTextView.setText("");
- }
- });
- }
-
- public void startAudioTest() {
- mThreadEnabled = true;
- mAutoThread = new Thread(this);
- mAutoThread.start();
- }
-
- // Only call from UI thread.
- @Override
- public void onTestFinished() {
- super.onTestFinished();
- }
-
- public void stopAudioTest() {
- try {
- if (mAutoThread != null) {
- mThreadEnabled = false;
- mAutoThread.interrupt();
- mAutoThread.join(100);
- mAutoThread = null;
- }
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
-
- // Share text from log via GMail, Drive or other method.
- public void onShareResult(View view) {
- Intent sharingIntent = new Intent(android.content.Intent.ACTION_SEND);
- sharingIntent.setType("text/plain");
-
- String subjectText = "OboeTester AutoGlitch result " + getTimestampString();
- sharingIntent.putExtra(android.content.Intent.EXTRA_SUBJECT, subjectText);
-
- String shareBody = mAutoTextView.getText().toString();
- sharingIntent.putExtra(android.content.Intent.EXTRA_TEXT, shareBody);
-
- startActivity(Intent.createChooser(sharingIntent, "Share using:"));
- }
-
- private String getConfigText(StreamConfiguration config) {
- return ((config.getDirection() == StreamConfiguration.DIRECTION_OUTPUT) ? "OUT" : "IN")
- + ", SR = " + config.getSampleRate()
- + ", Perf = " + StreamConfiguration.convertPerformanceModeToText(
- config.getPerformanceMode())
- + ", " + StreamConfiguration.convertSharingModeToText(config.getSharingMode())
- + ", ch = " + config.getChannelCount();
- }
-
- private void testConfiguration(int perfMode,
- int sharingMode,
- int sampleRate,
- int inChannels,
- int outChannels) throws InterruptedException {
-
- // Configure settings
- StreamConfiguration requestedInConfig = mAudioInputTester.requestedConfiguration;
- StreamConfiguration actualInConfig = mAudioInputTester.actualConfiguration;
- StreamConfiguration requestedOutConfig = mAudioOutTester.requestedConfiguration;
- StreamConfiguration actualOutConfig = mAudioOutTester.actualConfiguration;
-
- requestedInConfig.reset();
- requestedOutConfig.reset();
-
- requestedInConfig.setPerformanceMode(perfMode);
- requestedOutConfig.setPerformanceMode(perfMode);
-
- requestedInConfig.setSharingMode(sharingMode);
- requestedOutConfig.setSharingMode(sharingMode);
-
- requestedInConfig.setSampleRate(sampleRate);
- requestedOutConfig.setSampleRate(sampleRate);
-
- requestedInConfig.setChannelCount(inChannels);
- requestedOutConfig.setChannelCount(outChannels);
-
- log("========================== #" + mTestCount);
- log("Requested:");
- log(getConfigText(requestedInConfig));
- log(getConfigText(requestedOutConfig));
-
- // Give previous stream time to close and release resources. Avoid race conditions.
- Thread.sleep(1000);
- boolean openFailed = false;
- try {
- super.startAudioTest(); // this will fill in actualConfig
- log("Actual:");
- log(getConfigText(actualInConfig));
- log(getConfigText(actualOutConfig));
- // Set output size to a level that will avoid glitches.
- AudioStreamBase stream = mAudioOutTester.getCurrentAudioStream();
- int sizeFrames = stream.getBufferCapacityInFrames() / 2;
- stream.setBufferSizeInFrames(sizeFrames);
- } catch (IOException e) {
- openFailed = true;
- log(e.getMessage());
- }
-
- // The test would only be worth running if we got the configuration we requested on input or output.
- boolean valid = true;
- // No point running the test if we don't get the sharing mode we requested.
- if (!openFailed && actualInConfig.getSharingMode() != sharingMode
- && actualOutConfig.getSharingMode() != sharingMode) {
- log("did not get requested sharing mode");
- valid = false;
- }
- // We don't skip based on performance mode because if you request LOW_LATENCY you might
- // get a smaller burst than if you request NONE.
-
- if (!openFailed && valid) {
- Thread.sleep(mDurationSeconds * 1000);
- }
- int inXRuns = 0;
- int outXRuns = 0;
-
- if (!openFailed) {
- // get xRuns before closing the streams.
- inXRuns = mAudioInputTester.getCurrentAudioStream().getXRunCount();
- outXRuns = mAudioOutTester.getCurrentAudioStream().getXRunCount();
-
- super.stopAudioTest();
- }
-
- if (valid) {
- if (openFailed) {
- mFailedSummary.append("------ #" + mTestCount);
- mFailedSummary.append("\n");
- mFailedSummary.append(getConfigText(requestedInConfig));
- mFailedSummary.append("\n");
- mFailedSummary.append(getConfigText(requestedOutConfig));
- mFailedSummary.append("\n");
- mFailedSummary.append("Open failed!\n");
- mFailCount++;
- } else {
- log("Result:");
- boolean passed = (getMaxSecondsWithNoGlitch()
- > (mDurationSeconds - SETUP_TIME_SECONDS));
- String resultText = getShortReport();
- resultText += ", xruns = " + inXRuns + "/" + outXRuns;
- resultText += ", " + (passed ? TEXT_PASS : TEXT_FAIL);
- log(resultText);
- if (!passed) {
- mFailedSummary.append("------ #" + mTestCount);
- mFailedSummary.append("\n");
- mFailedSummary.append(" ");
- mFailedSummary.append(getConfigText(actualInConfig));
- mFailedSummary.append("\n");
- mFailedSummary.append(" ");
- mFailedSummary.append(resultText);
- mFailedSummary.append("\n");
- mFailCount++;
- } else {
- mPassCount++;
- }
- }
- } else {
- log(TEXT_SKIP);
- }
- // Give hardware time to settle between tests.
- Thread.sleep(mGapMillis);
- mTestCount++;
- }
-
- private void testConfiguration(int performanceMode,
- int sharingMode,
- int sampleRate) throws InterruptedException {
- testConfiguration(performanceMode,
- sharingMode,
- sampleRate, 1, 2);
- testConfiguration(performanceMode,
- sharingMode,
- sampleRate, 2, 1);
- }
-
-
- @Override
- public void run() {
- logClear();
- log("=== STARTED at " + new Date());
- log(Build.MANUFACTURER + " " + Build.PRODUCT);
- log(Build.DISPLAY);
- mFailedSummary = new StringBuffer();
- mTestCount = 0;
- mPassCount = 0;
- mFailCount = 0;
- try {
- testConfiguration(StreamConfiguration.PERFORMANCE_MODE_LOW_LATENCY,
- StreamConfiguration.SHARING_MODE_EXCLUSIVE,
- 0);
- testConfiguration(StreamConfiguration.PERFORMANCE_MODE_LOW_LATENCY,
- StreamConfiguration.SHARING_MODE_SHARED,
- 0);
-
- for (int perfMode : PERFORMANCE_MODES) {
- for (int sampleRate : SAMPLE_RATES) {
- testConfiguration(perfMode,
- StreamConfiguration.SHARING_MODE_SHARED,
- sampleRate);
- }
- }
- } catch (InterruptedException e) {
- e.printStackTrace();
- } finally {
- super.stopAudioTest();
- if (mThreadEnabled) {
- log("\n==== SUMMARY ========");
- if (mFailCount > 0) {
- log(mPassCount + " passed. " + mFailCount + " failed.");
- log("These tests FAILED:");
- log(mFailedSummary.toString());
- } else {
- log("All tests PASSED.");
- }
- log("== FINISHED at " + new Date());
- runOnUiThread(new Runnable() {
- @Override
- public void run() {
- onTestFinished();
- }
- });
- }
- }
- }
-
-}
diff --git a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/AutomatedGlitchActivity.java b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/AutomatedGlitchActivity.java
new file mode 100644
index 00000000..732ff92d
--- /dev/null
+++ b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/AutomatedGlitchActivity.java
@@ -0,0 +1,114 @@
+package com.google.sample.oboe.manualtest;
+
+import android.os.Bundle;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.Spinner;
+
+public class AutomatedGlitchActivity extends BaseAutoGlitchActivity {
+
+ private Spinner mDurationSpinner;
+
+ // Test with these configurations.
+ private static final int[] PERFORMANCE_MODES = {
+ StreamConfiguration.PERFORMANCE_MODE_LOW_LATENCY,
+ StreamConfiguration.PERFORMANCE_MODE_NONE
+ };
+ private static final int[] SAMPLE_RATES = { 48000, 44100, 16000 };
+ private static final int MONO = 1;
+ private static final int STEREO = 2;
+ private static final int UNSPECIFIED = 0;
+
+ private class DurationSpinnerListener implements android.widget.AdapterView.OnItemSelectedListener {
+ @Override
+ public void onItemSelected(AdapterView<?> parent, View view, int pos, long id) {
+ String text = parent.getItemAtPosition(pos).toString();
+ mDurationSeconds = Integer.parseInt(text);
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView<?> parent) {
+ mDurationSeconds = DEFAULT_DURATION_SECONDS;
+ }
+ }
+
+ @Override
+ protected void inflateActivity() {
+ setContentView(R.layout.activity_auto_glitches);
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ mDurationSpinner = (Spinner) findViewById(R.id.spinner_glitch_duration);
+ mDurationSpinner.setOnItemSelectedListener(new DurationSpinnerListener());
+ }
+
+ @Override
+ public String getTestName() {
+ return "AutoGlitch";
+ }
+
+ private void testConfiguration(int perfMode,
+ int sharingMode,
+ int sampleRate,
+ int inChannels,
+ int outChannels) throws InterruptedException {
+
+ // Configure settings
+ StreamConfiguration requestedInConfig = mAudioInputTester.requestedConfiguration;
+ StreamConfiguration requestedOutConfig = mAudioOutTester.requestedConfiguration;
+
+ requestedInConfig.reset();
+ requestedOutConfig.reset();
+
+ requestedInConfig.setPerformanceMode(perfMode);
+ requestedOutConfig.setPerformanceMode(perfMode);
+
+ requestedInConfig.setSharingMode(sharingMode);
+ requestedOutConfig.setSharingMode(sharingMode);
+
+ requestedInConfig.setSampleRate(sampleRate);
+ requestedOutConfig.setSampleRate(sampleRate);
+
+ requestedInConfig.setChannelCount(inChannels);
+ requestedOutConfig.setChannelCount(outChannels);
+
+ setTolerance(0.3f); // FIXME remove
+
+ testConfigurations();
+ }
+
+ private void testConfiguration(int performanceMode,
+ int sharingMode,
+ int sampleRate) throws InterruptedException {
+ testConfiguration(performanceMode,
+ sharingMode,
+ sampleRate, MONO, STEREO);
+ testConfiguration(performanceMode,
+ sharingMode,
+ sampleRate, STEREO, MONO);
+ }
+
+ @Override
+ public void runTest() {
+ try {
+ testConfiguration(StreamConfiguration.PERFORMANCE_MODE_LOW_LATENCY,
+ StreamConfiguration.SHARING_MODE_EXCLUSIVE,
+ UNSPECIFIED);
+
+ for (int perfMode : PERFORMANCE_MODES) {
+ for (int sampleRate : SAMPLE_RATES) {
+ testConfiguration(perfMode,
+ StreamConfiguration.SHARING_MODE_SHARED,
+ sampleRate);
+ }
+ }
+ } catch (InterruptedException e) {
+ log(e.getMessage());
+ showErrorToast(e.getMessage());
+ }
+ }
+
+}
diff --git a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/AutomatedTestRunner.java b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/AutomatedTestRunner.java
new file mode 100644
index 00000000..1f9714ff
--- /dev/null
+++ b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/AutomatedTestRunner.java
@@ -0,0 +1,285 @@
+package com.google.sample.oboe.manualtest;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build;
+import android.text.method.ScrollingMovementMethod;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.Button;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+
+/**
+ * Run an automated test from a UI, gather logs,
+ * and display a summary.
+ */
+public class AutomatedTestRunner extends LinearLayout implements Runnable {
+
+ private Button mStartButton;
+ private Button mStopButton;
+ private Button mShareButton;
+ private TextView mAutoTextView;
+ private TextView mSingleTestIndex;
+ private StringBuffer mFailedSummary;
+ private StringBuffer mSummary;
+ private int mTestCount;
+ private int mPassCount;
+ private int mFailCount;
+ private TestAudioActivity mActivity;
+
+ private Thread mAutoThread;
+ private volatile boolean mThreadEnabled;
+ private CachedTextViewLog mCachedTextView;
+
+ public AutomatedTestRunner(Context context) {
+ super(context);
+ initializeViews(context);
+ }
+
+ public AutomatedTestRunner(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ initializeViews(context);
+ }
+
+ public AutomatedTestRunner(Context context,
+ AttributeSet attrs,
+ int defStyle) {
+ super(context, attrs, defStyle);
+ initializeViews(context);
+ }
+
+ public TestAudioActivity getActivity() {
+ return mActivity;
+ }
+
+ public void setActivity(TestAudioActivity activity) {
+ this.mActivity = activity;
+ mCachedTextView = new CachedTextViewLog(activity, mAutoTextView);
+ }
+
+ /**
+ * Inflates the views in the layout.
+ *
+ * @param context
+ * the current context for the view.
+ */
+ private void initializeViews(Context context) {
+ LayoutInflater inflater = (LayoutInflater) context
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ inflater.inflate(R.layout.auto_test_runner, this);
+
+ mStartButton = (Button) findViewById(R.id.button_start);
+ mStartButton.setOnClickListener( new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ startTest();
+ }
+ });
+
+ mStopButton = (Button) findViewById(R.id.button_stop);
+ mStopButton.setOnClickListener( new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ stopTest();
+ }
+ });
+
+ mShareButton = (Button) findViewById(R.id.button_share);
+ mShareButton.setOnClickListener( new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ shareResult();
+ mShareButton.setEnabled(true);
+ }
+ });
+ mShareButton.setEnabled(false);
+
+ mSingleTestIndex = (TextView) findViewById(R.id.single_test_index);
+
+ mAutoTextView = (TextView) findViewById(R.id.text_log);
+ mAutoTextView.setMovementMethod(new ScrollingMovementMethod());
+ }
+
+ private void updateStartStopButtons(boolean running) {
+ mStartButton.setEnabled(!running);
+ mStopButton.setEnabled(running);
+ }
+
+ public int getTestCount() {
+ return mTestCount;
+ }
+
+ public boolean isThreadEnabled() {
+ return mThreadEnabled;
+ }
+
+ public void appendFailedSummary(String text) {
+ mFailedSummary.append(text);
+ }
+ public void appendSummary(String text) {
+ mSummary.append(text);
+ }
+
+ public void incrementFailCount() {
+ mFailCount++;
+ }
+ public void incrementPassCount() {
+ mPassCount++;
+ }
+ public void incrementTestCount() {
+ mTestCount++;
+ }
+
+ // Write to scrollable TextView
+ public void log(final String text) {
+ if (text == null) return;
+ Log.d(TestAudioActivity.TAG, "LOG - " + text);
+ mCachedTextView.append(text + "\n");
+ }
+
+ // Flush any logs that are stuck in the cache.
+ public void flushLog() {
+ mCachedTextView.flush();
+ }
+
+ private void logClear() {
+ mCachedTextView.clear();
+ }
+
+ private void startAutoThread() {
+ mThreadEnabled = true;
+ mAutoThread = new Thread(this);
+ mAutoThread.start();
+ }
+
+ private void stopAutoThread() {
+ try {
+ if (mAutoThread != null) {
+ log("Disable background test thread.");
+ new RuntimeException("Disable background test thread.").printStackTrace();
+ mThreadEnabled = false;
+ mAutoThread.interrupt();
+ mAutoThread.join(100);
+ mAutoThread = null;
+ }
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private void updateTestIndex() {
+ CharSequence chars = mSingleTestIndex.getText();
+ String text = chars.toString();
+ int testIndex = -1;
+ String trimmed = chars.toString().trim();
+ if (trimmed.length() > 0) {
+ try {
+ testIndex = Integer.parseInt(text);
+ } catch (NumberFormatException e) {
+ mActivity.showErrorToast("Badly formated callback size: " + text);
+ mSingleTestIndex.setText("");
+ }
+ }
+ mActivity.setSingleTestIndex(testIndex);
+ }
+
+ protected void startTest() {
+ updateTestIndex();
+ updateStartStopButtons(true);
+ startAutoThread();
+ }
+
+ public void stopTest() {
+ stopAutoThread();
+ }
+
+ // Only call from UI thread.
+ public void onTestFinished() {
+ updateStartStopButtons(false);
+ mShareButton.setEnabled(true);
+ }
+
+ public static String getTimestampString() {
+ DateFormat df = new SimpleDateFormat("yyyyMMdd-HHmmss");
+ Date now = Calendar.getInstance().getTime();
+ return df.format(now);
+ }
+
+ // Share text from log via GMail, Drive or other method.
+ public void shareResult() {
+ Intent sharingIntent = new Intent(android.content.Intent.ACTION_SEND);
+ sharingIntent.setType("text/plain");
+
+ String subjectText = "OboeTester-" + mActivity.getTestName()
+ + "-" + Build.MANUFACTURER
+ + "-" + Build.MODEL
+ + "-" + getTimestampString();
+ subjectText = subjectText.replace(' ', '-');
+ sharingIntent.putExtra(android.content.Intent.EXTRA_SUBJECT, subjectText);
+
+ String shareBody = mAutoTextView.getText().toString();
+ sharingIntent.putExtra(android.content.Intent.EXTRA_TEXT, shareBody);
+
+ mActivity.startActivity(Intent.createChooser(sharingIntent, "Share using:"));
+ }
+
+ @Override
+ public void run() {
+ logClear();
+ log("=== STARTED at " + new Date());
+ log(mActivity.getTestName());
+ log(MainActivity.getVersiontext());
+ log(Build.MANUFACTURER + ", " + Build.MODEL + ", " + Build.PRODUCT);
+ log(Build.DISPLAY);
+ mFailedSummary = new StringBuffer();
+ mSummary = new StringBuffer();
+ appendFailedSummary("Summary\n");
+ mTestCount = 0;
+ mPassCount = 0;
+ mFailCount = 0;
+ try {
+ mActivity.runTest();
+ log("Tests finished without exception.");
+ } catch(Exception e) {
+ log("EXCEPTION: " + e.getMessage());
+ } finally {
+ mActivity.stopTest();
+ if (mThreadEnabled) {
+ log("\n==== SUMMARY ========");
+ log(mSummary.toString());
+ if (mFailCount > 0) {
+ log("These tests FAILED:");
+ log(mFailedSummary.toString());
+ log("------------");
+ } else if (mPassCount > 0) {
+ log("All " + mPassCount + " tests PASSED.");
+ } else {
+ log("No tests were run!");
+ }
+ int skipped = mTestCount - (mPassCount + mFailCount);
+ log(mPassCount + " passed. "
+ + mFailCount + " failed. "
+ + skipped + " skipped. ");
+ log("== FINISHED at " + new Date());
+ } else {
+ log("== TEST STOPPED ==");
+ }
+ flushLog();
+ mActivity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ onTestFinished();
+ }
+ });
+ }
+ }
+
+}
diff --git a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/BaseAutoGlitchActivity.java b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/BaseAutoGlitchActivity.java
new file mode 100644
index 00000000..45d0e7ce
--- /dev/null
+++ b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/BaseAutoGlitchActivity.java
@@ -0,0 +1,227 @@
+/*
+ * Copyright 2019 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.
+ */
+
+package com.google.sample.oboe.manualtest;
+
+import android.os.Bundle;
+
+import java.io.IOException;
+
+public class BaseAutoGlitchActivity extends GlitchActivity {
+
+ private static final int SETUP_TIME_SECONDS = 4; // Time for the stream to settle.
+ protected static final int DEFAULT_DURATION_SECONDS = 8; // Run time for each test.
+ private static final int DEFAULT_GAP_MILLIS = 400; // Idle time between each test.
+ private static final String TEXT_SKIP = "SKIP";
+ public static final String TEXT_PASS = "PASS";
+ public static final String TEXT_FAIL = "FAIL !!!!";
+
+ protected int mDurationSeconds = DEFAULT_DURATION_SECONDS;
+ protected int mGapMillis = DEFAULT_GAP_MILLIS;
+
+ protected AutomatedTestRunner mAutomatedTestRunner;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ mAutomatedTestRunner = findViewById(R.id.auto_test_runner);
+ mAutomatedTestRunner.setActivity(this);
+ }
+
+ protected void log(String text) {
+ mAutomatedTestRunner.log(text);
+ }
+
+ protected void appendFailedSummary(String text) {
+ mAutomatedTestRunner.appendFailedSummary(text);
+ }
+
+ protected void appendSummary(String text) {
+ mAutomatedTestRunner.appendSummary(text);
+ }
+
+ @Override
+ public void onStopTest() {
+ mAutomatedTestRunner.stopTest();
+ }
+
+ protected String getConfigText(StreamConfiguration config) {
+ int channel = (config.getDirection() == StreamConfiguration.DIRECTION_OUTPUT)
+ ? getOutputChannel() : getInputChannel();
+ return ((config.getDirection() == StreamConfiguration.DIRECTION_OUTPUT) ? "OUT" : "INP")
+ + (config.isMMap() ? "-M" : "-L")
+ + ", ID = " + String.format("%2d", config.getDeviceId())
+ + ", SR = " + String.format("%5d", config.getSampleRate())
+ + ", Perf = " + StreamConfiguration.convertPerformanceModeToText(
+ config.getPerformanceMode())
+ + ", " + StreamConfiguration.convertSharingModeToText(config.getSharingMode())
+ + ", ch = " + config.getChannelCount() + "[" + channel + "]";
+ }
+
+ public final static int TEST_RESULT_FAILED = -2;
+ public final static int TEST_RESULT_WARNING = -1;
+ public final static int TEST_RESULT_SKIPPED = 0;
+ public final static int TEST_RESULT_PASSED = 1;
+
+ // Run test based on the requested input/output configurations.
+ protected int testConfigurations() throws InterruptedException {
+ int result = TEST_RESULT_SKIPPED;
+ mAutomatedTestRunner.incrementTestCount();
+ if ((getSingleTestIndex() >= 0) && (mAutomatedTestRunner.getTestCount() != getSingleTestIndex())) {
+ return result;
+ }
+
+ log("========================== #" + mAutomatedTestRunner.getTestCount());
+
+ StreamConfiguration requestedInConfig = mAudioInputTester.requestedConfiguration;
+ StreamConfiguration requestedOutConfig = mAudioOutTester.requestedConfiguration;
+
+ StreamConfiguration actualInConfig = mAudioInputTester.actualConfiguration;
+ StreamConfiguration actualOutConfig = mAudioOutTester.actualConfiguration;
+
+ log("Requested:");
+ log(" " + getConfigText(requestedInConfig));
+ log(" " + getConfigText(requestedOutConfig));
+
+ String reason = "";
+ boolean openFailed = false;
+ try {
+ openAudio(); // this will fill in actualConfig
+ log("Actual:");
+ log(" " + getConfigText(actualInConfig));
+ log(" " + getConfigText(actualOutConfig));
+ // Set output size to a level that will avoid glitches.
+ AudioStreamBase stream = mAudioOutTester.getCurrentAudioStream();
+ int sizeFrames = stream.getBufferCapacityInFrames() / 2;
+ stream.setBufferSizeInFrames(sizeFrames);
+ } catch (Exception e) {
+ openFailed = true;
+ log(e.getMessage());
+ reason = e.getMessage();
+ }
+
+ // The test would only be worth running if we got the configuration we requested on input or output.
+ String skipReason = shouldTestBeSkipped();
+ boolean skipped = skipReason.length() > 0;
+ boolean valid = !openFailed && !skipped;
+ boolean startFailed = false;
+ if (valid) {
+ try {
+ startAudioTest();
+ } catch (IOException e) {
+ e.printStackTrace();
+ valid = false;
+ startFailed = true;
+ log(e.getMessage());
+ reason = e.getMessage();
+ }
+ }
+ mAutomatedTestRunner.flushLog();
+
+ if (valid) {
+ // Check for early return until we reach full duration.
+ long now = System.currentTimeMillis();
+ long startedAt = now;
+ long endTime = System.currentTimeMillis() + (mDurationSeconds * 1000);
+ boolean finishedEarly = false;
+ while (now < endTime && !finishedEarly) {
+ Thread.sleep(100); // Let test run.
+ now = System.currentTimeMillis();
+ finishedEarly = isFinishedEarly();
+ if (finishedEarly) {
+ log("Finished early after " + (now - startedAt) + " msec.");
+ }
+ }
+ }
+ int inXRuns = 0;
+ int outXRuns = 0;
+
+ if (!openFailed) {
+ // get xRuns before closing the streams.
+ inXRuns = mAudioInputTester.getCurrentAudioStream().getXRunCount();
+ outXRuns = mAudioOutTester.getCurrentAudioStream().getXRunCount();
+
+ super.stopAudioTest();
+ }
+
+ if (openFailed || startFailed) {
+ appendFailedSummary("------ #" + mAutomatedTestRunner.getTestCount() + "\n");
+ appendFailedSummary(getConfigText(requestedInConfig) + "\n");
+ appendFailedSummary(getConfigText(requestedOutConfig) + "\n");
+ appendFailedSummary(reason + "\n");
+ mAutomatedTestRunner.incrementFailCount();
+ } else if (skipped) {
+ log(TEXT_SKIP + " - " + skipReason);
+ } else {
+ log("Result:");
+ reason += didTestFail();
+ boolean passed = reason.length() == 0;
+
+ String resultText = getShortReport();
+ resultText += ", xruns = " + inXRuns + "/" + outXRuns;
+ resultText += ", " + (passed ? TEXT_PASS : TEXT_FAIL);
+ resultText += reason;
+ log(" " + resultText);
+ if (!passed) {
+ appendFailedSummary("------ #" + mAutomatedTestRunner.getTestCount() + "\n");
+ appendFailedSummary(" " + getConfigText(actualInConfig) + "\n");
+ appendFailedSummary(" " + getConfigText(actualOutConfig) + "\n");
+ appendFailedSummary(" " + resultText + "\n");
+ mAutomatedTestRunner.incrementFailCount();
+ result = TEST_RESULT_FAILED;
+ } else {
+ mAutomatedTestRunner.incrementPassCount();
+ result = TEST_RESULT_PASSED;
+ }
+ }
+ mAutomatedTestRunner.flushLog();
+
+ // Give hardware time to settle between tests.
+ Thread.sleep(mGapMillis);
+ return result;
+ }
+
+ protected boolean isFinishedEarly() {
+ return false;
+ }
+
+ protected String shouldTestBeSkipped() {
+ String why = "";
+ StreamConfiguration requestedInConfig = mAudioInputTester.requestedConfiguration;
+ StreamConfiguration requestedOutConfig = mAudioOutTester.requestedConfiguration;
+ StreamConfiguration actualInConfig = mAudioInputTester.actualConfiguration;
+ StreamConfiguration actualOutConfig = mAudioOutTester.actualConfiguration;
+ // No point running the test if we don't get the sharing mode we requested.
+ if (actualInConfig.getSharingMode() != requestedInConfig.getSharingMode()
+ || actualOutConfig.getSharingMode() != requestedOutConfig.getSharingMode()) {
+ log("Did not get requested sharing mode.");
+ why += "share";
+ }
+ // We don't skip based on performance mode because if you request LOW_LATENCY you might
+ // get a smaller burst than if you request NONE.
+ return why;
+ }
+
+ public String didTestFail() {
+ String why = "";
+ if (getMaxSecondsWithNoGlitch() <= (mDurationSeconds - SETUP_TIME_SECONDS)) {
+ why += ", glitch";
+ }
+ return why;
+ }
+
+}
diff --git a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/BufferSizeView.java b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/BufferSizeView.java
index 04ce9413..cd7d36f3 100644
--- a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/BufferSizeView.java
+++ b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/BufferSizeView.java
@@ -21,23 +21,33 @@ import android.util.AttributeSet;
import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.RadioButton;
import android.widget.SeekBar;
import android.widget.TextView;
import android.widget.LinearLayout;
public class BufferSizeView extends LinearLayout {
-
- AudioOutputTester mAudioOutTester;
-
- protected static final int FADER_THRESHOLD_MAX = 1000; // must match layout
- protected TextView mTextThreshold;
- protected SeekBar mFaderThreshold;
- protected ExponentialTaper mTaperThreshold;
+ private OboeAudioStream mStream;
+
+ private static final int FADER_THRESHOLD_MAX = 1000; // must match layout
+ private static final int USE_FADER = -1;
+ private static final int DEFAULT_NUM_BURSTS = 2;
+ private TextView mTextLabel;
+ private SeekBar mFader;
+ private ExponentialTaper mTaper;
+ private RadioButton mBufferSizeRadio1;
+ private RadioButton mBufferSizeRadio2;
+ private RadioButton mBufferSizeRadio3;
private int mCachedCapacity;
+ private int mFramesPerBurst;
+ private int mNumBursts;
- private SeekBar.OnSeekBarChangeListener mThresholdListener = new SeekBar.OnSeekBarChangeListener() {
+ private SeekBar.OnSeekBarChangeListener mFaderListener = new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ mNumBursts = USE_FADER;
+ updateRadioButtons();
setBufferSizeByPosition(progress);
}
@@ -61,83 +71,145 @@ public class BufferSizeView extends LinearLayout {
}
public BufferSizeView(Context context,
- AttributeSet attrs,
- int defStyle) {
+ AttributeSet attrs,
+ int defStyle) {
super(context, attrs, defStyle);
initializeViews(context);
}
- public AudioOutputTester getAudioOutTester() {
- return mAudioOutTester;
- }
-
- public void setAudioOutTester(AudioOutputTester audioOutTester) {
- mAudioOutTester = audioOutTester;
- }
-
void setFaderNormalizedProgress(double fraction) {
- mFaderThreshold.setProgress((int)(fraction * FADER_THRESHOLD_MAX));
+ mFader.setProgress((int) (fraction * FADER_THRESHOLD_MAX));
}
/**
* Inflates the views in the layout.
*
- * @param context
- * the current context for the view.
+ * @param context the current context for the view.
*/
private void initializeViews(Context context) {
LayoutInflater inflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate(R.layout.buffer_size_view, this);
- mTextThreshold = (TextView) findViewById(R.id.textThreshold);
- mFaderThreshold = (SeekBar) findViewById(R.id.faderThreshold);
- mFaderThreshold.setOnSeekBarChangeListener(mThresholdListener);
- mTaperThreshold = new ExponentialTaper(0.0, 1.0, 10.0);
- mFaderThreshold.setProgress(0);
+ mTextLabel = (TextView) findViewById(R.id.textThreshold);
+ mFader = (SeekBar) findViewById(R.id.faderBufferSize);
+ mFader.setOnSeekBarChangeListener(mFaderListener);
+ mTaper = new ExponentialTaper(0.0, 1.0, 10.0);
+ mFader.setProgress(0);
+
+ mBufferSizeRadio1 = (RadioButton) findViewById(R.id.bufferSize1);
+ mBufferSizeRadio1.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ onSizeRadioButtonClicked(view, 1);
+ }
+ });
+ mBufferSizeRadio2 = (RadioButton) findViewById(R.id.bufferSize2);
+ mBufferSizeRadio2.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ onSizeRadioButtonClicked(view, 2);
+ }
+ });
+ mBufferSizeRadio3 = (RadioButton) findViewById(R.id.bufferSize3);
+ mBufferSizeRadio3.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ onSizeRadioButtonClicked(view, 3);
+ }
+ });
+ mNumBursts = DEFAULT_NUM_BURSTS;
+ updateRadioButtons();
+ updateBufferSize();
+ }
+
+ public void updateRadioButtons() {
+ if (mBufferSizeRadio3 != null) {
+ mBufferSizeRadio1.setChecked(mNumBursts == 1);
+ mBufferSizeRadio2.setChecked(mNumBursts == 2);
+ mBufferSizeRadio3.setChecked(mNumBursts == 3);
+ }
+ }
+
+ private void onSizeRadioButtonClicked(View view, int numBursts) {
+ boolean checked = ((RadioButton) view).isChecked();
+ if (!checked) return;
+ mNumBursts = numBursts;
+ setBufferSizeByNumBursts(numBursts);
+ }
+
+ // sets mStream, mCachedCapacity and mFramesPerBurst
+ public void onStreamOpened(OboeAudioStream stream) {
+ mStream = stream;
+ if (mStream != null) {
+ int capacity = mStream.getBufferCapacityInFrames();
+ if (capacity > 0) mCachedCapacity = capacity;
+ int framesPerBurst = mStream.getFramesPerBurst();
+ if (framesPerBurst > 0) mFramesPerBurst = framesPerBurst;
+ }
+ updateBufferSize();
+ }
+
+ private void setBufferSizeByNumBursts(int numBursts) {
+ int sizeFrames = -1;
+ if (mStream != null) {
+ int framesPerBurst = mStream.getFramesPerBurst();
+ if (framesPerBurst > 0) {
+ sizeFrames = numBursts * framesPerBurst;
+ }
+ }
+ StringBuffer message = new StringBuffer();
+ message.append("bufferSize = #" + numBursts);
+
+ setBufferSize(message, sizeFrames);
}
private void setBufferSizeByPosition(int progress) {
+ int sizeFrames = -1;
+ double normalizedThreshold = 0.0;
+
StringBuffer message = new StringBuffer();
- double normalizedThreshold = mTaperThreshold.linearToExponential(
- ((double)progress)/FADER_THRESHOLD_MAX);
+
+ normalizedThreshold = mTaper.linearToExponential(
+ ((double) progress) / FADER_THRESHOLD_MAX);
if (normalizedThreshold < 0.0) normalizedThreshold = 0.0;
else if (normalizedThreshold > 1.0) normalizedThreshold = 1.0;
- int percent = (int) (normalizedThreshold * 100);
+ int percent = (int) (normalizedThreshold * 100);
message.append("bufferSize = " + percent + "%");
- OboeAudioStream stream = null;
- int sizeFrames = 0;
- if (getAudioOutTester() != null) {
- stream = (OboeAudioStream) getAudioOutTester().getCurrentAudioStream();
- if (stream != null) {
- int capacity = stream.getBufferCapacityInFrames();
- if (capacity > 0) mCachedCapacity = capacity;
- }
- }
if (mCachedCapacity > 0) {
sizeFrames = (int) (normalizedThreshold * mCachedCapacity);
- message.append(" = " + sizeFrames);
- if (stream != null) {
- stream.setBufferSizeInFrames(sizeFrames);
+ }
+ setBufferSize(message, sizeFrames);
+ }
+
+ private void setBufferSize(StringBuffer message, int sizeFrames) {
+ if (mStream != null) {
+ message.append(", " + sizeFrames);
+ if (mStream != null && sizeFrames >= 0) {
+ mStream.setBufferSizeInFrames(sizeFrames);
}
- int bufferSize = getAudioOutTester().getCurrentAudioStream().getBufferSizeInFrames();
+ int bufferSize = mStream.getBufferSizeInFrames();
if (bufferSize >= 0) {
message.append(" / " + bufferSize);
}
message.append(" / " + mCachedCapacity);
}
- mTextThreshold.setText(message.toString());
+ mTextLabel.setText(message.toString());
}
- public void updateBufferSize() {
- int progress = mFaderThreshold.getProgress();
- setBufferSizeByPosition(progress);
+ private void updateBufferSize() {
+ if (mNumBursts == USE_FADER) {
+ int progress = mFader.getProgress();
+ setBufferSizeByPosition(progress);
+ } else {
+ setBufferSizeByNumBursts(mNumBursts);
+ }
}
@Override
public void setEnabled(boolean enabled) {
super.setEnabled(enabled);
- mFaderThreshold.setEnabled(enabled);
+ mFader.setEnabled(enabled);
}
}
diff --git a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/CachedTextViewLog.java b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/CachedTextViewLog.java
new file mode 100644
index 00000000..dfb6a165
--- /dev/null
+++ b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/CachedTextViewLog.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2020 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.
+ */
+
+package com.google.sample.oboe.manualtest;
+
+import android.app.Activity;
+import android.widget.TextView;
+
+/** Wrap a TextView with a buffer and only update it periodically.
+ */
+public class CachedTextViewLog {
+ private final Activity mActivity;
+ private TextView mTextView;
+ private int mUpdatePeriodMillis = 500;
+ private long mLastUpdateTime;
+ private StringBuffer mBuffer = new StringBuffer();
+
+ public CachedTextViewLog(Activity activity, TextView textView) {
+ mActivity = activity;
+ mTextView = textView;
+ }
+
+ public synchronized void append(String text) {
+ mBuffer.append(text);
+ long now = System.currentTimeMillis();
+ if ((now - mLastUpdateTime) > mUpdatePeriodMillis) {
+ flush_l();
+ }
+ }
+
+ public synchronized void flush() {
+ flush_l();
+ }
+
+ // This must be called from a synchronized method.
+ private void flush_l() {
+ final String textToAdd = mBuffer.toString();
+ mBuffer.setLength(0);
+ mLastUpdateTime = System.currentTimeMillis();
+ mActivity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mTextView.append(textToAdd);
+ }
+ });
+ }
+
+ public synchronized void clear() {
+ mBuffer.setLength(0);
+ mLastUpdateTime = System.currentTimeMillis();
+ mActivity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mTextView.setText("");
+ }
+ });
+ }
+
+}
diff --git a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/EchoActivity.java b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/EchoActivity.java
index 688d1abb..29026fa0 100644
--- a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/EchoActivity.java
+++ b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/EchoActivity.java
@@ -16,6 +16,7 @@
package com.google.sample.oboe.manualtest;
+import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
@@ -40,6 +41,9 @@ public class EchoActivity extends TestInputActivity {
private double mDelayTime;
private Button mStartButton;
private Button mStopButton;
+ private TextView mStatusTextView;
+
+ private ColdStartSniffer mNativeSniffer = new ColdStartSniffer(this);
protected static final int MAX_DELAY_TIME_PROGRESS = 1000;
@@ -58,6 +62,74 @@ public class EchoActivity extends TestInputActivity {
}
};
+ // Periodically query for cold start latency from the native code until it is ready.
+ protected class ColdStartSniffer extends NativeSniffer {
+
+ private int stableCallCount = 0;
+ private static final int STABLE_CALLS_NEEDED = 20;
+ private int mInputLatency;
+ private int mOutputLatency;
+
+ public ColdStartSniffer(Activity activity) {
+ super(activity);
+ }
+
+ @Override
+ public void startSniffer() {
+ stableCallCount = 0;
+ mInputLatency = -1;
+ mOutputLatency = -1;
+ super.startSniffer();
+ }
+
+ public void run() {
+ mInputLatency = getColdStartInputMillis();
+ mOutputLatency = getColdStartOutputMillis();
+ updateStatusText();
+ if (!isComplete()) {
+ reschedule();
+ }
+ }
+
+ private boolean isComplete() {
+ if (mInputLatency > 0 && mOutputLatency > 0) {
+ stableCallCount++;
+ }
+ return stableCallCount > STABLE_CALLS_NEEDED;
+ }
+
+ private String getCurrentStatusReport() {
+ StringBuffer message = new StringBuffer();
+ message.append("cold.start.input.msec = " +
+ ((mInputLatency > 0)
+ ? mInputLatency
+ : "?")
+ + "\n");
+ message.append("cold.start.output.msec = " +
+ ((mOutputLatency > 0)
+ ? mOutputLatency
+ : "?")
+ + "\n");
+ message.append("stable.call.count = " + stableCallCount + "\n");
+ return message.toString();
+ }
+
+ @Override
+ public String getShortReport() {
+ return getCurrentStatusReport();
+ }
+
+ @Override
+ public void updateStatusText() {
+ String message = getCurrentStatusReport();
+ mStatusTextView.setText(message);
+ }
+
+ }
+
+ private native int getColdStartInputMillis();
+ private native int getColdStartOutputMillis();
+
@Override
protected void inflateActivity() {
setContentView(R.layout.activity_echo);
@@ -75,6 +147,8 @@ public class EchoActivity extends TestInputActivity {
mStopButton = (Button) findViewById(R.id.button_stop_echo);
mStopButton.setEnabled(false);
+ mStatusTextView = (TextView) findViewById(R.id.text_status);
+
mTextDelayTime = (TextView) findViewById(R.id.text_delay_time);
mFaderDelayTime = (SeekBar) findViewById(R.id.fader_delay_time);
mFaderDelayTime.setOnSeekBarChangeListener(mDelayListener);
@@ -96,12 +170,11 @@ public class EchoActivity extends TestInputActivity {
private native void setDelayTime(double delayTimeSeconds);
- @Override
- protected void onStart() {
- super.onStart();
- setActivityType(ACTIVITY_ECHO);
+ int getActivityType() {
+ return ACTIVITY_ECHO;
}
+
@Override
protected void resetConfiguration() {
super.resetConfiguration();
@@ -116,12 +189,14 @@ public class EchoActivity extends TestInputActivity {
mStartButton.setEnabled(false);
mStopButton.setEnabled(true);
keepScreenOn(true);
+ mNativeSniffer.startSniffer();
} catch (IOException e) {
showErrorToast(e.getMessage());
}
}
public void onStopEcho(View view) {
+ mNativeSniffer.stopSniffer();
stopAudio();
closeAudio();
mStartButton.setEnabled(true);
diff --git a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/GlitchActivity.java b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/GlitchActivity.java
index 4fb9f1fb..01ac1d1f 100644
--- a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/GlitchActivity.java
+++ b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/GlitchActivity.java
@@ -16,9 +16,8 @@
package com.google.sample.oboe.manualtest;
+import android.app.Activity;
import android.os.Bundle;
-import android.os.Handler;
-import android.os.Looper;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
@@ -42,12 +41,24 @@ public class GlitchActivity extends AnalyzerActivity {
final static int STATE_LOCKED = 4;
final static int STATE_GLITCHING = 5;
String mLastGlitchReport;
+ private int mInputChannel;
+ private int mOutputChannel;
native int getStateFrameCount(int state);
native int getGlitchCount();
native double getSignalToNoiseDB();
native double getPeakAmplitude();
+ private GlitchSniffer mGlitchSniffer;
+ private NativeSniffer mNativeSniffer = createNativeSniffer();
+
+ synchronized NativeSniffer createNativeSniffer() {
+ if (mGlitchSniffer == null) {
+ mGlitchSniffer = new GlitchSniffer(this);
+ }
+ return mGlitchSniffer;
+ }
+
// Note that these strings must match the enum result_code in LatencyAnalyzer.h
String stateToString(int resultCode) {
switch (resultCode) {
@@ -69,9 +80,7 @@ public class GlitchActivity extends AnalyzerActivity {
}
// Periodically query for glitches from the native detector.
- protected class GlitchSniffer {
- public static final int SNIFFER_UPDATE_PERIOD_MSEC = 100;
- public static final int SNIFFER_UPDATE_DELAY_MSEC = 200;
+ protected class GlitchSniffer extends NativeSniffer {
private long mTimeAtStart;
private long mTimeOfLastGlitch;
@@ -88,10 +97,14 @@ public class GlitchActivity extends AnalyzerActivity {
private double mSignalToNoiseDB;
private double mPeakAmplitude;
- private Handler mHandler = new Handler(Looper.getMainLooper()); // UI thread
- private volatile boolean mEnabled = true;
- private void startSniffer() {
+ public GlitchSniffer(Activity activity) {
+ super(activity);
+ }
+
+
+ @Override
+ public void startSniffer() {
long now = System.currentTimeMillis();
mTimeAtStart = now;
mTimeOfLastGlitch = now;
@@ -102,71 +115,47 @@ public class GlitchActivity extends AnalyzerActivity {
mMaxSecondsWithoutGlitches = 0.0;
mLastGlitchCount = 0;
mStartResetCount = mLastResetCount;
- // Start the initial runnable task by posting through the handler
- mEnabled = true;
- mHandler.postDelayed(runnableCode, SNIFFER_UPDATE_DELAY_MSEC);
+ super.startSniffer();
}
- private void stopSniffer() {
- mEnabled = false;
- if (mHandler != null) {
- mHandler.removeCallbacks(runnableCode);
- }
+ public void run() {
+ int state = getAnalyzerState();
+ mSignalToNoiseDB = getSignalToNoiseDB();
+ mPeakAmplitude = getPeakAmplitude();
+ mPreviousState = state;
- runOnUiThread(new Runnable() {
- @Override
- public void run() {
- updateStatusText();
- }
- });
- }
+ long now = System.currentTimeMillis();
+ int glitchCount = getGlitchCount();
+ int resetCount = getResetCount();
+ mLastUnlockedFrames = getStateFrameCount(STATE_WAITING_FOR_LOCK);
+ int lockedFrames = getStateFrameCount(STATE_LOCKED);
+ int glitchFrames = getStateFrameCount(STATE_GLITCHING);
+
+ if (glitchFrames > mLastGlitchFrames || glitchCount > mLastGlitchCount) {
+ mTimeOfLastGlitch = now;
+ mSecondsWithoutGlitches = 0.0;
+ onGlitchDetected();
+ } else if (lockedFrames > mLastLockedFrames) {
+ mSecondsWithoutGlitches = (now - mTimeOfLastGlitch) / 1000.0;
+ }
- private Runnable runnableCode = new Runnable() {
- @Override
- public void run() {
- int state = getAnalyzerState();
- mSignalToNoiseDB = getSignalToNoiseDB();
- mPeakAmplitude = getPeakAmplitude();
- mPreviousState = state;
-
- long now = System.currentTimeMillis();
- int glitchCount = getGlitchCount();
- int resetCount = getResetCount();
- mLastUnlockedFrames = getStateFrameCount(STATE_WAITING_FOR_LOCK);
- int lockedFrames = getStateFrameCount(STATE_LOCKED);
- int glitchFrames = getStateFrameCount(STATE_GLITCHING);
-
- if (glitchFrames > mLastGlitchFrames || glitchCount > mLastGlitchCount) {
- mTimeOfLastGlitch = now;
- mSecondsWithoutGlitches = 0.0;
- onGlitchDetected();
- } else if (lockedFrames > mLastLockedFrames) {
- mSecondsWithoutGlitches = (now - mTimeOfLastGlitch) / 1000.0;
- }
-
- if (resetCount > mLastResetCount) {
- mLastResetCount = resetCount;
- }
-
- if (mSecondsWithoutGlitches > mMaxSecondsWithoutGlitches) {
- mMaxSecondsWithoutGlitches = mSecondsWithoutGlitches;
- }
-
- mLastGlitchCount = glitchCount;
- mLastGlitchFrames = glitchFrames;
- mLastLockedFrames = lockedFrames;
+ if (resetCount > mLastResetCount) {
mLastResetCount = resetCount;
+ }
- updateStatusText();
-
- // Reschedule so this task repeats
- if (mEnabled) {
- mHandler.postDelayed(runnableCode, SNIFFER_UPDATE_PERIOD_MSEC);
- }
+ if (mSecondsWithoutGlitches > mMaxSecondsWithoutGlitches) {
+ mMaxSecondsWithoutGlitches = mSecondsWithoutGlitches;
}
- };
- String getCurrentStatusReport() {
+ mLastGlitchCount = glitchCount;
+ mLastGlitchFrames = glitchFrames;
+ mLastLockedFrames = lockedFrames;
+ mLastResetCount = resetCount;
+
+ reschedule();
+ }
+
+ private String getCurrentStatusReport() {
long now = System.currentTimeMillis();
double totalSeconds = (now - mTimeAtStart) / 1000.0;
@@ -190,6 +179,7 @@ public class GlitchActivity extends AnalyzerActivity {
return message.toString();
}
+ @Override
public String getShortReport() {
String resultText = "#glitches = " + getLastGlitchCount()
+ ", #resets = " + getLastResetCount()
@@ -199,7 +189,8 @@ public class GlitchActivity extends AnalyzerActivity {
return resultText;
}
- private void updateStatusText() {
+ @Override
+ public void updateStatusText() {
mLastGlitchReport = getCurrentStatusReport();
setAnalyzerText(mLastGlitchReport);
}
@@ -220,9 +211,7 @@ public class GlitchActivity extends AnalyzerActivity {
protected void onGlitchDetected() {
}
- private GlitchSniffer mGlitchSniffer = new GlitchSniffer();
-
- private void setAnalyzerText(String s) {
+ protected void setAnalyzerText(String s) {
mAnalyzerTextView.setText(s);
}
@@ -233,6 +222,28 @@ public class GlitchActivity extends AnalyzerActivity {
*/
public native void setTolerance(float tolerance);
+ public void setInputChannel(int channel) {
+ mInputChannel = channel;
+ setInputChannelNative(channel);
+ }
+
+ public void setOutputChannel(int channel) {
+ mOutputChannel = channel;
+ setOutputChannelNative(channel);
+ }
+
+ public int getInputChannel() {
+ return mInputChannel;
+ }
+
+ public int getOutputChannel() {
+ return mOutputChannel;
+ }
+
+ public native void setInputChannelNative(int channel);
+
+ public native void setOutputChannelNative(int channel);
+
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -254,9 +265,13 @@ public class GlitchActivity extends AnalyzerActivity {
}
@Override
+ int getActivityType() {
+ return ACTIVITY_GLITCHES;
+ }
+
+ @Override
protected void onStart() {
super.onStart();
- setActivityType(ACTIVITY_GLITCHES);
mStartButton.setEnabled(true);
mStopButton.setEnabled(false);
mShareButton.setEnabled(false);
@@ -270,6 +285,7 @@ public class GlitchActivity extends AnalyzerActivity {
// Called on UI thread
public void onStartAudioTest(View view) throws IOException {
+ openAudio();
startAudioTest();
mStartButton.setEnabled(false);
mStopButton.setEnabled(true);
@@ -278,9 +294,8 @@ public class GlitchActivity extends AnalyzerActivity {
}
public void startAudioTest() throws IOException {
- openAudio();
startAudio();
- mGlitchSniffer.startSniffer();
+ mNativeSniffer.startSniffer();
onTestBegan();
}
@@ -308,11 +323,15 @@ public class GlitchActivity extends AnalyzerActivity {
}
public void stopAudioTest() {
- mGlitchSniffer.stopSniffer();
+ mNativeSniffer.stopSniffer();
stopAudio();
closeAudio();
}
+ public void stopTest() {
+ stopAudio();
+ }
+
@Override
boolean isOutput() {
return false;
@@ -327,7 +346,7 @@ public class GlitchActivity extends AnalyzerActivity {
}
public String getShortReport() {
- return mGlitchSniffer.getShortReport();
+ return mNativeSniffer.getShortReport();
}
@Override
diff --git a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/MainActivity.java b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/MainActivity.java
index 099774e2..0a4be582 100644
--- a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/MainActivity.java
+++ b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/MainActivity.java
@@ -53,7 +53,7 @@ public class MainActivity extends Activity {
private Spinner mModeSpinner;
- private TextView mCallbackSizeTextView;
+ private TextView mCallbackSizeEditor;
protected TextView mDeviceView;
private TextView mVersionTextView;
private TextView mBuildTextView;
@@ -61,6 +61,7 @@ public class MainActivity extends Activity {
private Bundle mBundleFromIntent;
private BroadcastReceiver mScoStateReceiver;
private CheckBox mWorkaroundsCheckBox;
+ private static String mVersionText;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -70,7 +71,7 @@ public class MainActivity extends Activity {
logScreenSize();
mVersionTextView = (TextView) findViewById(R.id.versionText);
- mCallbackSizeTextView = (TextView) findViewById(R.id.callbackSize);
+ mCallbackSizeEditor = (TextView) findViewById(R.id.callbackSize);
mDeviceView = (TextView) findViewById(R.id.deviceView);
updateNativeAudioUI();
@@ -96,8 +97,9 @@ public class MainActivity extends Activity {
int oboeMajor = (oboeVersion >> 24) & 0xFF;
int oboeMinor = (oboeVersion >> 16) & 0xFF;
int oboePatch = oboeVersion & 0xFF;
- mVersionTextView.setText("Test v (" + pinfo.versionCode + ") " + pinfo.versionName
- + ", Oboe v " + oboeMajor + "." + oboeMinor + "." + oboePatch);
+ mVersionText = "OboeTester (" + pinfo.versionCode + ") v " + pinfo.versionName
+ + ", Oboe v " + oboeMajor + "." + oboeMinor + "." + oboePatch;
+ mVersionTextView.setText(mVersionText);
} catch (PackageManager.NameNotFoundException e) {
mVersionTextView.setText(e.getMessage());
}
@@ -127,6 +129,10 @@ public class MainActivity extends Activity {
saveIntentBundleForLaterProcessing(getIntent());
}
+ public static String getVersiontext() {
+ return mVersionText;
+ }
+
private void registerScoStateReceiver() {
registerReceiver(mScoStateReceiver,
new IntentFilter(AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED));
@@ -198,62 +204,50 @@ public class MainActivity extends Activity {
}
public void onLaunchTestOutput(View view) {
- updateCallbackSize();
- Intent intent = new Intent(this, TestOutputActivity.class);
- startActivity(intent);
+ onLaunchTest(TestOutputActivity.class);
}
public void onLaunchTestInput(View view) {
- updateCallbackSize();
- Intent intent = new Intent(this, TestInputActivity.class);
- startActivity(intent);
+ onLaunchTest(TestInputActivity.class);
}
public void onLaunchTapToTone(View view) {
- updateCallbackSize();
- Intent intent = new Intent(this, TapToToneActivity.class);
- startActivity(intent);
+ onLaunchTest(TapToToneActivity.class);
}
public void onLaunchRecorder(View view) {
- updateCallbackSize();
- Intent intent = new Intent(this, RecorderActivity.class);
- startActivity(intent);
+ onLaunchTest(RecorderActivity.class);
}
public void onLaunchEcho(View view) {
- updateCallbackSize();
- Intent intent = new Intent(this, EchoActivity.class);
- startActivity(intent);
+ onLaunchTest(EchoActivity.class);
}
public void onLaunchRoundTripLatency(View view) {
- updateCallbackSize();
- Intent intent = new Intent(this, RoundTripLatencyActivity.class);
- startActivity(intent);
+ onLaunchTest(RoundTripLatencyActivity.class);
}
public void onLaunchManualGlitchTest(View view) {
- updateCallbackSize();
- Intent intent = new Intent(this, ManualGlitchActivity.class);
- startActivity(intent);
+ onLaunchTest(ManualGlitchActivity.class);
}
- public void onLaunchAutoGlitchTest(View view) {
- updateCallbackSize();
- Intent intent = new Intent(this, AutoGlitchActivity.class);
- startActivity(intent);
- }
+ public void onLaunchAutoGlitchTest(View view) { onLaunchTest(AutomatedGlitchActivity.class); }
public void onLaunchTestDisconnect(View view) {
- updateCallbackSize();
- Intent intent = new Intent(this, TestDisconnectActivity.class);
- startActivity(intent);
+ onLaunchTest(TestDisconnectActivity.class);
+ }
+
+ public void onLaunchTestDataPaths(View view) {
+ onLaunchTest(TestDataPathsActivity.class);
+ }
+
+ public void onLaunchTestDeviceReport(View view) {
+ onLaunchTest(DeviceReportActivity.class);
}
- public void onLaunchTestDeviceReport(View view) {
+ private void onLaunchTest(Class clazz) {
updateCallbackSize();
- Intent intent = new Intent(this, DeviceReportActivity.class);
+ Intent intent = new Intent(this, clazz);
startActivity(intent);
}
@@ -278,14 +272,14 @@ public class MainActivity extends Activity {
}
private void updateCallbackSize() {
- CharSequence chars = mCallbackSizeTextView.getText();
+ CharSequence chars = mCallbackSizeEditor.getText();
String text = chars.toString();
int callbackSize = 0;
try {
callbackSize = Integer.parseInt(text);
} catch (NumberFormatException e) {
showErrorToast("Badly formated callback size: " + text);
- mCallbackSizeTextView.setText("0");
+ mCallbackSizeEditor.setText("0");
}
OboeAudioStream.setCallbackSize(callbackSize);
}
diff --git a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/NativeSniffer.java b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/NativeSniffer.java
new file mode 100644
index 00000000..e2c95c16
--- /dev/null
+++ b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/NativeSniffer.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2018 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.
+ */
+
+package com.google.sample.oboe.manualtest;
+
+import android.app.Activity;
+import android.os.Handler;
+import android.os.Looper;
+
+abstract class NativeSniffer implements Runnable {
+ public static final int SNIFFER_UPDATE_PERIOD_MSEC = 100;
+ public static final int SNIFFER_UPDATE_DELAY_MSEC = 200;
+ private final Activity activity;
+ protected Handler mHandler = new Handler(Looper.getMainLooper()); // UI thread
+ protected volatile boolean mEnabled = true;
+
+ public NativeSniffer(Activity activity) {
+ this.activity = activity;
+ }
+
+ public void startSniffer() {
+ long now = System.currentTimeMillis();
+ // Start the initial runnable task by posting through the handler
+ mEnabled = true;
+ mHandler.postDelayed(this, SNIFFER_UPDATE_DELAY_MSEC);
+ }
+
+ public void stopSniffer() {
+ mEnabled = false;
+ if (mHandler != null) {
+ mHandler.removeCallbacks(this);
+ }
+
+ activity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ updateStatusText();
+ }
+ });
+ }
+
+ public void reschedule() {
+ updateStatusText();
+ // Reschedule so this task repeats
+ if (mEnabled) {
+ mHandler.postDelayed(this, SNIFFER_UPDATE_PERIOD_MSEC);
+ }
+ }
+
+ public abstract void updateStatusText();
+
+ public String getShortReport() {
+ return "no-report";
+ }
+
+}
diff --git a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/OboeAudioStream.java b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/OboeAudioStream.java
index 32462f15..9576800d 100644
--- a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/OboeAudioStream.java
+++ b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/OboeAudioStream.java
@@ -229,9 +229,9 @@ abstract class OboeAudioStream extends AudioStreamBase {
@Override
public double getLatency() {
- return getLatency(streamIndex);
+ return getTimestampLatency(streamIndex);
}
- public native double getLatency(int streamIndex);
+ public native double getTimestampLatency(int streamIndex);
@Override
public double getCpuLoad() {
diff --git a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/RecorderActivity.java b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/RecorderActivity.java
index cc4ed736..1ceb06ab 100644
--- a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/RecorderActivity.java
+++ b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/RecorderActivity.java
@@ -54,10 +54,8 @@ public class RecorderActivity extends TestInputActivity {
updateButtons();
}
- @Override
- protected void onStart() {
- super.onStart();
- setActivityType(ACTIVITY_RECORD_PLAY);
+ int getActivityType() {
+ return ACTIVITY_RECORD_PLAY;
}
public void onStartRecording(View view) {
diff --git a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/RoundTripLatencyActivity.java b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/RoundTripLatencyActivity.java
index 211987e6..5c939295 100644
--- a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/RoundTripLatencyActivity.java
+++ b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/RoundTripLatencyActivity.java
@@ -21,19 +21,25 @@ import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.support.annotation.NonNull;
+import android.text.method.ScrollingMovementMethod;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import java.io.IOException;
+import java.util.ArrayList;
/**
* Activity to measure latency on a full duplex stream.
*/
public class RoundTripLatencyActivity extends AnalyzerActivity {
- private static final int STATE_GOT_DATA = 2; // Defined in LatencyAnalyzer.h
+ // STATEs defined in LatencyAnalyzer.h
+ private static final int STATE_MEASURE_BACKGROUND = 0;
+ private static final int STATE_IN_PULSE = 1;
+ private static final int STATE_GOT_DATA = 2;
private final static String LATENCY_FORMAT = "%4.2f";
+ // When I use 5.3g I only get one digit after the decimal point!
private final static String CONFIDENCE_FORMAT = "%5.3f";
private TextView mAnalyzerView;
@@ -51,12 +57,13 @@ public class RoundTripLatencyActivity extends AnalyzerActivity {
// Run the test several times and report the acverage latency.
protected class LatencyAverager {
private final static int AVERAGE_TEST_DELAY_MSEC = 1000; // arbitrary
- private static final int GOOD_RUNS_REQUIRED = 10; // arbitrary
- private static final int MAX_BAD_RUNS_ALLOWED = 10; // arbitrary
+ private static final int GOOD_RUNS_REQUIRED = 5; // arbitrary
+ private static final int MAX_BAD_RUNS_ALLOWED = 5; // arbitrary
private int mBadCount = 0; // number of bad measurements
private int mGoodCount = 0; // number of good measurements
- private double mWeightedLatencySum;
+ ArrayList<Double> mLatencies = new ArrayList<Double>(GOOD_RUNS_REQUIRED);
+ ArrayList<Double> mConfidences = new ArrayList<Double>(GOOD_RUNS_REQUIRED);
private double mLatencyMin;
private double mLatencyMax;
private double mConfidenceSum;
@@ -84,7 +91,8 @@ public class RoundTripLatencyActivity extends AnalyzerActivity {
mGoodCount++;
double latency = getMeasuredLatencyMillis();
double confidence = getMeasuredConfidence();
- mWeightedLatencySum += latency * confidence; // weighted average based on confidence
+ mLatencies.add(latency);
+ mConfidences.add(confidence);
mConfidenceSum += confidence;
mLatencyMin = Math.min(mLatencyMin, latency);
mLatencyMax = Math.max(mLatencyMax, latency);
@@ -112,11 +120,11 @@ public class RoundTripLatencyActivity extends AnalyzerActivity {
if (mGoodCount == 0 || mConfidenceSum == 0.0) {
message = "num.iterations = " + mGoodCount + "\n";
} else {
- // When I use 5.3g I only get one digit after the decimal point!
- final double averageLatency = mWeightedLatencySum / mConfidenceSum;
final double mAverageConfidence = mConfidenceSum / mGoodCount;
- message =
- "average.latency.msec = " + String.format(LATENCY_FORMAT, averageLatency) + "\n"
+ double meanLatency = calculateMeanLatency();
+ double meanAbsoluteDeviation = calculateMeanAbsoluteDeviation(meanLatency);
+ message = "average.latency.msec = " + String.format(LATENCY_FORMAT, meanLatency) + "\n"
+ + "mean.absolute.deviation = " + String.format(LATENCY_FORMAT, meanAbsoluteDeviation) + "\n"
+ "average.confidence = " + String.format(CONFIDENCE_FORMAT, mAverageConfidence) + "\n"
+ "min.latency.msec = " + String.format(LATENCY_FORMAT, mLatencyMin) + "\n"
+ "max.latency.msec = " + String.format(LATENCY_FORMAT, mLatencyMax) + "\n"
@@ -127,9 +135,26 @@ public class RoundTripLatencyActivity extends AnalyzerActivity {
return message;
}
+ private double calculateMeanAbsoluteDeviation(double meanLatency) {
+ double deviationSum = 0.0;
+ for (double latency : mLatencies) {
+ deviationSum += Math.abs(latency - meanLatency);
+ }
+ return deviationSum / mLatencies.size();
+ }
+
+ private double calculateMeanLatency() {
+ double latencySum = 0.0;
+ for (double latency : mLatencies) {
+ latencySum += latency;
+ }
+ return latencySum / mLatencies.size();
+ }
+
// Called on UI thread.
public void start() {
- mWeightedLatencySum = 0.0;
+ mLatencies.clear();
+ mConfidences.clear();
mConfidenceSum = 0.0;
mLatencyMax = Double.MIN_VALUE;
mLatencyMin = Double.MAX_VALUE;
@@ -165,7 +190,6 @@ public class RoundTripLatencyActivity extends AnalyzerActivity {
public static final int SNIFFER_UPDATE_PERIOD_MSEC = 150;
public static final int SNIFFER_UPDATE_DELAY_MSEC = 300;
-
// Display status info for the stream.
private Runnable runnableCode = new Runnable() {
@Override
@@ -173,14 +197,13 @@ public class RoundTripLatencyActivity extends AnalyzerActivity {
String message;
if (isAnalyzerDone()) {
- message = onAnalyzerDone();
- message += mLatencyAverager.onAnalyserDone();
+ message = mLatencyAverager.onAnalyserDone();
+ message += onAnalyzerDone();
} else {
message = getProgressText();
message += "please wait... " + counter + "\n";
- if (getAnalyzerState() == STATE_GOT_DATA) {
- message += "ANALYZING\n";
- }
+ message += convertStateToString(getAnalyzerState());
+
// Repeat this runnable code block again.
mHandler.postDelayed(runnableCode, SNIFFER_UPDATE_PERIOD_MSEC);
}
@@ -202,11 +225,20 @@ public class RoundTripLatencyActivity extends AnalyzerActivity {
}
}
+ static String convertStateToString(int state) {
+ switch (state) {
+ case STATE_MEASURE_BACKGROUND: return "BACKGROUND";
+ case STATE_IN_PULSE: return "RECORDING";
+ case STATE_GOT_DATA: return "ANALYZING";
+ default: return "DONE";
+ }
+ }
+
private String getProgressText() {
int progress = getAnalyzerProgress();
int state = getAnalyzerState();
int resetCount = getResetCount();
- String message = String.format("progress = %d, state = %d, #resets = %d\n",
+ String message = String.format("progress = %d\nstate = %d\n#resets = %d\n",
progress, state, resetCount);
message += mLatencyAverager.getLastReport();
return message;
@@ -227,13 +259,12 @@ public class RoundTripLatencyActivity extends AnalyzerActivity {
@NonNull
private String getResultString() {
- String message = String.format("rms.signal = %7.5f\n", getSignalRMS());
- message += String.format("rms.noise = %7.5f\n", getBackgroundRMS());
+ int result = getMeasuredResult();
int resetCount = getResetCount();
- message += String.format("reset.count = %d\n", resetCount);
+ double confidence = getMeasuredConfidence();
+ String message = "";
- int result = getMeasuredResult();
- message += String.format("result = %d\n", result);
+ message += String.format("confidence = " + CONFIDENCE_FORMAT + "\n", confidence);
message += String.format("result.text = %s\n", resultCodeToString(result));
// Only report valid latencies.
@@ -243,13 +274,17 @@ public class RoundTripLatencyActivity extends AnalyzerActivity {
int bufferSize = mAudioOutTester.getCurrentAudioStream().getBufferSizeInFrames();
int latencyEmptyFrames = latencyFrames - bufferSize;
double latencyEmptyMillis = latencyEmptyFrames * 1000.0 / getSampleRate();
- message += String.format("latency.empty.frames = %d\n", latencyEmptyFrames);
- message += String.format("latency.empty.msec = " + LATENCY_FORMAT + "\n", latencyEmptyMillis);
- message += String.format("latency.frames = %d\n", latencyFrames);
message += String.format("latency.msec = " + LATENCY_FORMAT + "\n", latencyMillis);
+ message += String.format("latency.frames = %d\n", latencyFrames);
+ message += String.format("latency.empty.msec = " + LATENCY_FORMAT + "\n", latencyEmptyMillis);
+ message += String.format("latency.empty.frames = %d\n", latencyEmptyFrames);
}
- double confidence = getMeasuredConfidence();
- message += String.format("confidence = " + CONFIDENCE_FORMAT + "\n", confidence);
+
+ message += String.format("rms.signal = %7.5f\n", getSignalRMS());
+ message += String.format("rms.noise = %7.5f\n", getBackgroundRMS());
+ message += String.format("reset.count = %d\n", resetCount);
+ message += String.format("result = %d\n", result);
+
return message;
}
@@ -282,6 +317,7 @@ public class RoundTripLatencyActivity extends AnalyzerActivity {
mShareButton = (Button) findViewById(R.id.button_share);
mShareButton.setEnabled(false);
mAnalyzerView = (TextView) findViewById(R.id.text_status);
+ mAnalyzerView.setMovementMethod(new ScrollingMovementMethod());
updateEnabledWidgets();
hideSettingsViews();
@@ -297,9 +333,13 @@ public class RoundTripLatencyActivity extends AnalyzerActivity {
}
@Override
+ int getActivityType() {
+ return ACTIVITY_RT_LATENCY;
+ }
+
+ @Override
protected void onStart() {
super.onStart();
- setActivityType(ACTIVITY_RT_LATENCY);
mHasRecording = false;
updateButtons(false);
}
diff --git a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/StreamConfiguration.java b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/StreamConfiguration.java
index 07112b12..8ad101eb 100644
--- a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/StreamConfiguration.java
+++ b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/StreamConfiguration.java
@@ -146,26 +146,27 @@ public class StreamConfiguration {
this.mPerformanceMode = performanceMode;
}
- public int getInputPreset() {
- return mInputPreset;
- }
- public void setInputPreset(int inputPreset) {
- this.mInputPreset = inputPreset;
- }
-
static String convertPerformanceModeToText(int performanceMode) {
switch(performanceMode) {
case PERFORMANCE_MODE_NONE:
- return "NONE";
+ return "NO";
case PERFORMANCE_MODE_POWER_SAVING:
- return "PWRSAV";
+ return "PS";
case PERFORMANCE_MODE_LOW_LATENCY:
- return "LOWLAT";
+ return "LL";
default:
- return "INVALID";
+ return "??";
}
}
+ public int getInputPreset() {
+ return mInputPreset;
+ }
+
+ public void setInputPreset(int inputPreset) {
+ this.mInputPreset = inputPreset;
+ }
+
public int getSharingMode() {
return mSharingMode;
}
@@ -177,11 +178,11 @@ public class StreamConfiguration {
static String convertSharingModeToText(int sharingMode) {
switch(sharingMode) {
case SHARING_MODE_SHARED:
- return "SHARED";
+ return "SH";
case SHARING_MODE_EXCLUSIVE:
- return "EXCLUSIVE";
+ return "EX";
default:
- return "INVALID";
+ return "??";
}
}
@@ -322,9 +323,8 @@ public class StreamConfiguration {
public boolean isMMap() {
return mMMap;
}
- public void setMMap(boolean b) {
- mMMap = b;
- }
+
+ public void setMMap(boolean b) { mMMap = b; }
public int getNativeApi() {
return mNativeApi;
diff --git a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TapLatencyAnalyser.java b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TapLatencyAnalyser.java
index 9f902769..09e4e970 100644
--- a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TapLatencyAnalyser.java
+++ b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TapLatencyAnalyser.java
@@ -50,7 +50,7 @@ public class TapLatencyAnalyser {
private void highPassFilter(float[] buffer, int offset, int numSamples, float[] highPassBuffer) {
float xn1 = 0.0f;
float yn1 = 0.0f;
- float alpha = 0.05f;
+ final float alpha = 0.05f;
for (int i = 0; i < numSamples; i++) {
float xn = buffer[i + offset];
float yn = alpha * yn1 + ((1.0f - alpha) * (xn - xn1));
@@ -87,22 +87,14 @@ public class TapLatencyAnalyser {
private void fillPeakBuffer(float[] buffer, int offset, int numSamples, float[] peakBuffer) {
float previous = 0.0f;
- float maxInput = 0.0f;
- float maxOutput = 0.0f;
for (int i = 0; i < numSamples; i++) {
float input = buffer[i + offset];
- if (input > maxInput) {
- maxInput = input;
- }
float output = previous * mDroop;
if (input > output) {
output = input;
}
previous = output;
peakBuffer[i] = output;
- if (output > maxOutput) {
- maxOutput = output;
- }
}
}
}
diff --git a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TapToToneActivity.java b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TapToToneActivity.java
index b32ccfd6..cdb46f06 100644
--- a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TapToToneActivity.java
+++ b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TapToToneActivity.java
@@ -108,9 +108,8 @@ public class TapToToneActivity extends TestOutputActivityBase {
}
@Override
- protected void onStart() {
- super.onStart();
- setActivityType(ACTIVITY_TAP_TO_TONE);
+ int getActivityType() {
+ return ACTIVITY_TAP_TO_TONE;
}
@Override
@@ -333,7 +332,7 @@ public class TapToToneActivity extends TestOutputActivityBase {
}
@Override
- public void startAudio() {
+ public void startAudio() throws IOException {
if (hasRecordAudioPermission()) {
startAudioPermitted();
} else {
@@ -341,14 +340,10 @@ public class TapToToneActivity extends TestOutputActivityBase {
}
}
- private void startAudioPermitted() {
+ private void startAudioPermitted() throws IOException {
super.startAudio();
resetLatency();
- try {
- mAudioMidiTester.start();
- } catch (IOException e) {
- e.printStackTrace();
- }
+ mAudioMidiTester.start();
}
@Override
diff --git a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestAudioActivity.java b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestAudioActivity.java
index bd7fff50..a68eb3a4 100644
--- a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestAudioActivity.java
+++ b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestAudioActivity.java
@@ -31,17 +31,13 @@ import android.widget.CheckBox;
import android.widget.Toast;
import java.io.IOException;
-import java.text.DateFormat;
-import java.text.SimpleDateFormat;
import java.util.ArrayList;
-import java.util.Calendar;
-import java.util.Date;
/**
* Base class for other Activities.
*/
abstract class TestAudioActivity extends Activity {
- public static final String TAG = "TestOboe";
+ public static final String TAG = "OboeTester";
protected static final int FADER_PROGRESS_MAX = 1000;
@@ -65,6 +61,7 @@ abstract class TestAudioActivity extends Activity {
public static final int ACTIVITY_RT_LATENCY = 5;
public static final int ACTIVITY_GLITCHES = 6;
public static final int ACTIVITY_TEST_DISCONNECT = 7;
+ public static final int ACTIVITY_DATA_PATHS = 8;
private int mAudioState = AUDIO_STATE_CLOSED;
protected String audioManagerSampleRate;
@@ -79,6 +76,11 @@ abstract class TestAudioActivity extends Activity {
private CheckBox mCallbackReturnStopBox;
private int mSampleRate;
private boolean mScoStarted;
+ private int mSingleTestIndex = -1;
+
+ public String getTestName() {
+ return "TestAudio";
+ }
public static class StreamContext {
StreamConfigurationView configurationView;
@@ -104,11 +106,15 @@ abstract class TestAudioActivity extends Activity {
boolean gotViews = false;
for (StreamContext streamContext : mStreamContexts) {
AudioStreamBase.StreamStatus status = streamContext.tester.getCurrentAudioStream().getStreamStatus();
+ AudioStreamBase.DoubleStatistics latencyStatistics =
+ streamContext.tester.getCurrentAudioStream().getLatencyStatistics();
if (streamContext.configurationView != null) {
// Handler runs this on the main UI thread.
int framesPerBurst = streamContext.tester.getCurrentAudioStream().getFramesPerBurst();
status.framesPerCallback = getFramesPerCallback();
- final String msg = status.dump(framesPerBurst);
+ String msg = "";
+ msg += "timestamp.latency = " + latencyStatistics.dump() + "\n";
+ msg += status.dump(framesPerBurst);
streamContext.configurationView.setStatusText(msg);
updateStreamDisplay();
gotViews = true;
@@ -166,10 +172,20 @@ abstract class TestAudioActivity extends Activity {
}
}
+ abstract int getActivityType();
+
+ public void setSingleTestIndex(int testIndex) {
+ mSingleTestIndex = testIndex;
+ }
+ public int getSingleTestIndex() {
+ return mSingleTestIndex;
+ }
+
@Override
protected void onStart() {
super.onStart();
resetConfiguration();
+ setActivityType(getActivityType());
}
protected void resetConfiguration() {
@@ -177,9 +193,8 @@ abstract class TestAudioActivity extends Activity {
@Override
protected void onStop() {
- Log.i(TAG, "onStop() called so stopping audio =========================");
- stopAudio();
- closeAudio();
+ Log.i(TAG, "onStop() called so stop the test =========================");
+ onStopTest();
super.onStop();
}
@@ -346,7 +361,11 @@ abstract class TestAudioActivity extends Activity {
public void startAudio(View view) {
Log.i(TAG, "startAudio() called =======================================");
- startAudio();
+ try {
+ startAudio();
+ } catch (Exception e) {
+ showErrorToast(e.getMessage());
+ }
keepScreenOn(true);
}
@@ -452,10 +471,11 @@ abstract class TestAudioActivity extends Activity {
protected native void setActivityType(int activityType);
private native int getFramesPerCallback();
- public void startAudio() {
+ public void startAudio() throws IOException {
int result = startNative();
if (result < 0) {
showErrorToast("Start failed with " + result);
+ throw new IOException("startNative returned " + result);
} else {
for (StreamContext streamContext : mStreamContexts) {
StreamConfigurationView configView = streamContext.configurationView;
@@ -492,6 +512,18 @@ abstract class TestAudioActivity extends Activity {
}
}
+ public void runTest() {}
+
+ // This should only be called from UI events such as onStop or a button press.
+ public void onStopTest() {
+ stopTest();
+ }
+
+ public void stopTest() {
+ stopAudio();
+ closeAudio();
+ }
+
public void stopAudioQuiet() {
stopNative();
mAudioState = AUDIO_STATE_STOPPED;
@@ -529,12 +561,6 @@ abstract class TestAudioActivity extends Activity {
updateEnabledWidgets();
}
- String getTimestampString() {
- DateFormat df = new SimpleDateFormat("yyyyMMdd-HHmmss");
- Date now = Calendar.getInstance().getTime();
- return df.format(now);
- }
-
void startBluetoothSco() {
AudioManager myAudioMgr = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
myAudioMgr.startBluetoothSco();
@@ -544,4 +570,5 @@ abstract class TestAudioActivity extends Activity {
AudioManager myAudioMgr = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
myAudioMgr.stopBluetoothSco();
}
+
}
diff --git a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestDataPathsActivity.java b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestDataPathsActivity.java
new file mode 100644
index 00000000..9c6676c5
--- /dev/null
+++ b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestDataPathsActivity.java
@@ -0,0 +1,512 @@
+/*
+ * Copyright 2020 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.
+ */
+
+package com.google.sample.oboe.manualtest;
+
+import android.app.Activity;
+import android.content.Context;
+import android.media.AudioDeviceInfo;
+import android.media.AudioManager;
+import android.os.Bundle;
+
+import com.google.sample.audio_device.AudioDeviceInfoConverter;
+
+/**
+ * Play a recognizable tone on each channel of each speaker device
+ * and listen for the result through a microphone.
+ * Also test each microphone channel and device.
+ * Try each InputPreset.
+ *
+ * The analysis is based on a cosine transform of a single
+ * frequency. The magnitude indicates the level.
+ * The variations in phase, "jitter" indicate how noisy the
+ * signal is or whether it is corrupted. A noisy room may have
+ * energy at the target frequency but the phase will be random.
+ *
+ * This test requires a quiet room but no other hardware.
+ */
+public class TestDataPathsActivity extends BaseAutoGlitchActivity {
+
+ public static final int DURATION_SECONDS = 3;
+ private final static double MIN_REQUIRED_MAGNITUDE = 0.001;
+ private final static double MAX_SINE_FREQUENCY = 1000.0;
+ private final static int TYPICAL_SAMPLE_RATE = 48000;
+ private final static double FRAMES_PER_CYCLE = TYPICAL_SAMPLE_RATE / MAX_SINE_FREQUENCY;
+ private final static double PHASE_PER_BIN = 2.0 * Math.PI / FRAMES_PER_CYCLE;
+ private final static double MAX_ALLOWED_JITTER = 0.5 * PHASE_PER_BIN;
+ // Start by failing then let good results drive us into a pass value.
+ private final static double INITIAL_JITTER = 2.0 * MAX_ALLOWED_JITTER;
+ // A coefficient of 0.0 is no filtering. 0.9999 is extreme low pass.
+ private final static double JITTER_FILTER_COEFFICIENT = 0.8;
+ private final static String MAGNITUDE_FORMAT = "%7.5f";
+
+ final int TYPE_BUILTIN_SPEAKER_SAFE = 0x18; // API 30
+
+ private double mMagnitude;
+ private double mMaxMagnitude;
+ private int mPhaseCount;
+ private double mPhase;
+ private double mPhaseJitter;
+
+ AudioManager mAudioManager;
+
+ private static final int[] INPUT_PRESETS = {
+ // VOICE_RECOGNITION gets tested in testInputs()
+ // StreamConfiguration.INPUT_PRESET_VOICE_RECOGNITION,
+ StreamConfiguration.INPUT_PRESET_GENERIC,
+ StreamConfiguration.INPUT_PRESET_CAMCORDER,
+ // TODO Resolve issue with echo cancellation killing the signal.
+ // TODO StreamConfiguration.INPUT_PRESET_VOICE_COMMUNICATION,
+ StreamConfiguration.INPUT_PRESET_UNPROCESSED,
+ StreamConfiguration.INPUT_PRESET_VOICE_PERFORMANCE,
+ };
+
+ // Periodically query for magnitude and phase from the native detector.
+ protected class DataPathSniffer extends NativeSniffer {
+
+ public DataPathSniffer(Activity activity) {
+ super(activity);
+ }
+
+ @Override
+ public void startSniffer() {
+ mMagnitude = -1.0;
+ mMaxMagnitude = -1.0;
+ mPhaseCount = 0;
+ mPhase = 0.0;
+ mPhaseJitter = INITIAL_JITTER;
+ super.startSniffer();
+ }
+
+ @Override
+ public void run() {
+ mMagnitude = getMagnitude();
+ mMaxMagnitude = getMaxMagnitude();
+ // Only look at the phase if we have a signal.
+ if (mMagnitude >= MIN_REQUIRED_MAGNITUDE) {
+ double phase = getPhase();
+ if (mPhaseCount > 3) {
+ double diff = Math.abs(phase - mPhase);
+ // low pass filter
+ mPhaseJitter = (mPhaseJitter * JITTER_FILTER_COEFFICIENT)
+ + ((diff * (1.0 - JITTER_FILTER_COEFFICIENT)));
+ }
+ mPhase = phase;
+ mPhaseCount++;
+ }
+ reschedule();
+ }
+
+ public String getCurrentStatusReport() {
+ StringBuffer message = new StringBuffer();
+ message.append(
+ "magnitude = " + getMagnitudeText(mMagnitude)
+ + ", max = " + getMagnitudeText(mMaxMagnitude)
+ + "\nphase = " + getMagnitudeText(mPhase)
+ + ", jitter = " + getMagnitudeText(mPhaseJitter)
+ + "\n");
+ return message.toString();
+ }
+
+ @Override
+ public String getShortReport() {
+ return "maxMag = " + getMagnitudeText(mMaxMagnitude)
+ + ", jitter = " + getMagnitudeText(mPhaseJitter);
+ }
+
+ @Override
+ public void updateStatusText() {
+ mLastGlitchReport = getCurrentStatusReport();
+ setAnalyzerText(mLastGlitchReport);
+ }
+
+ }
+
+ @Override
+ NativeSniffer createNativeSniffer() {
+ return new TestDataPathsActivity.DataPathSniffer(this);
+ }
+
+ native double getMagnitude();
+ native double getMaxMagnitude();
+ native double getPhase();
+
+ @Override
+ protected void inflateActivity() {
+ setContentView(R.layout.activity_data_paths);
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
+ }
+
+ @Override
+ public String getTestName() {
+ return "DataPaths";
+ }
+
+ @Override
+ int getActivityType() {
+ return ACTIVITY_DATA_PATHS;
+ }
+
+ String getMagnitudeText(double value) {
+ return String.format(MAGNITUDE_FORMAT, value);
+ }
+
+ protected String getConfigText(StreamConfiguration config) {
+ String text = super.getConfigText(config);
+ if (config.getDirection() == StreamConfiguration.DIRECTION_INPUT) {
+ text += ", inPre = " + StreamConfiguration.convertInputPresetToText(config.getInputPreset());
+ }
+ return text;
+ }
+
+ @Override
+ protected String shouldTestBeSkipped() {
+ String why = "";
+ StreamConfiguration requestedInConfig = mAudioInputTester.requestedConfiguration;
+ StreamConfiguration requestedOutConfig = mAudioOutTester.requestedConfiguration;
+ StreamConfiguration actualInConfig = mAudioInputTester.actualConfiguration;
+ StreamConfiguration actualOutConfig = mAudioOutTester.actualConfiguration;
+ // No point running the test if we don't get the sharing mode we requested.
+ if (actualInConfig.isMMap() != requestedInConfig.isMMap()
+ || actualOutConfig.isMMap() != requestedOutConfig.isMMap()) {
+ log("Did not get requested MMap stream");
+ why += "mmap";
+ } // Did we request a device and not get that device?
+ if (requestedInConfig.getDeviceId() != 0
+ && (requestedInConfig.getDeviceId() != actualInConfig.getDeviceId())) {
+ why += ", inDev(" + requestedInConfig.getDeviceId()
+ + "!=" + actualInConfig.getDeviceId() + ")";
+ }
+ if (requestedOutConfig.getDeviceId() != 0
+ && (requestedOutConfig.getDeviceId() != actualOutConfig.getDeviceId())) {
+ why += ", outDev(" + requestedOutConfig.getDeviceId()
+ + "!=" + actualOutConfig.getDeviceId() + ")";
+ }
+ if ((requestedInConfig.getInputPreset() != actualInConfig.getInputPreset())) {
+ why += ", inPre(" + requestedInConfig.getInputPreset()
+ + "!=" + actualInConfig.getInputPreset() + ")";
+ }
+ return why;
+ }
+
+ @Override
+ protected boolean isFinishedEarly() {
+ return (mMaxMagnitude > MIN_REQUIRED_MAGNITUDE) && (mPhaseJitter < MAX_ALLOWED_JITTER);
+ }
+
+ // @return reasons for failure of empty string
+ @Override
+ public String didTestFail() {
+ String why = "";
+ StreamConfiguration requestedInConfig = mAudioInputTester.requestedConfiguration;
+ StreamConfiguration requestedOutConfig = mAudioOutTester.requestedConfiguration;
+ StreamConfiguration actualInConfig = mAudioInputTester.actualConfiguration;
+ StreamConfiguration actualOutConfig = mAudioOutTester.actualConfiguration;
+ boolean passed = true;
+ if (mMaxMagnitude <= MIN_REQUIRED_MAGNITUDE) {
+ why += ", mag";
+ }
+ if (mPhaseJitter > MAX_ALLOWED_JITTER) {
+ why += ", jitter";
+ }
+ return why;
+ }
+
+ String getOneLineSummary() {
+ StreamConfiguration actualInConfig = mAudioInputTester.actualConfiguration;
+ StreamConfiguration actualOutConfig = mAudioOutTester.actualConfiguration;
+ return "#" + mAutomatedTestRunner.getTestCount()
+ + ", IN" + (actualInConfig.isMMap() ? "-M" : "-L")
+ + " D=" + actualInConfig.getDeviceId()
+ + ", ch=" + actualInConfig.getChannelCount() + "[" + getInputChannel() + "]"
+ + ", OUT" + (actualOutConfig.isMMap() ? "-M" : "-L")
+ + " D=" + (actualOutConfig.isMMap() ? "-M" : "-L")
+ + ", ch=" + actualOutConfig.getChannelCount() + "[" + getOutputChannel() + "]"
+ + ", mag = " + getMagnitudeText(mMaxMagnitude);
+ }
+
+ void setupDeviceCombo(int numInputChannels,
+ int inputChannel,
+ int numOutputChannels,
+ int outputChannel) throws InterruptedException {
+ // Configure settings
+ StreamConfiguration requestedInConfig = mAudioInputTester.requestedConfiguration;
+ StreamConfiguration requestedOutConfig = mAudioOutTester.requestedConfiguration;
+
+ requestedInConfig.reset();
+ requestedOutConfig.reset();
+
+ requestedInConfig.setPerformanceMode(StreamConfiguration.PERFORMANCE_MODE_LOW_LATENCY);
+ requestedOutConfig.setPerformanceMode(StreamConfiguration.PERFORMANCE_MODE_LOW_LATENCY);
+
+ requestedInConfig.setSharingMode(StreamConfiguration.SHARING_MODE_SHARED);
+ requestedOutConfig.setSharingMode(StreamConfiguration.SHARING_MODE_SHARED);
+
+ requestedInConfig.setChannelCount(numInputChannels);
+ requestedOutConfig.setChannelCount(numOutputChannels);
+
+ requestedInConfig.setMMap(false);
+ requestedOutConfig.setMMap(false);
+
+ setInputChannel(inputChannel);
+ setOutputChannel(outputChannel);
+ }
+
+ void testPresetCombo(int inputPreset,
+ int numInputChannels,
+ int inputChannel,
+ int numOutputChannels,
+ int outputChannel,
+ boolean mmapEnabled
+ ) throws InterruptedException {
+
+ setupDeviceCombo(numInputChannels, inputChannel, numOutputChannels, outputChannel);
+
+ StreamConfiguration requestedInConfig = mAudioInputTester.requestedConfiguration;
+ requestedInConfig.setInputPreset(inputPreset);
+ requestedInConfig.setMMap(mmapEnabled);
+
+ mMagnitude = -1.0;
+ int result = testConfigurations();
+ if (result != TEST_RESULT_SKIPPED) {
+ String summary = getOneLineSummary()
+ + ", inPre = "
+ + StreamConfiguration.convertInputPresetToText(inputPreset)
+ + "\n";
+ appendSummary(summary);
+ if (result == TEST_RESULT_FAILED) {
+ if (inputPreset == StreamConfiguration.INPUT_PRESET_VOICE_COMMUNICATION) {
+ logFailed("Maybe sine wave blocked by Echo Cancellation!");
+ }
+ }
+ }
+ }
+
+ void testPresetCombo(int inputPreset,
+ int numInputChannels,
+ int inputChannel,
+ int numOutputChannels,
+ int outputChannel
+ ) throws InterruptedException {
+ if (NativeEngine.isMMapSupported()) {
+ testPresetCombo(inputPreset, numInputChannels, inputChannel,
+ numOutputChannels, outputChannel, true);
+ }
+ testPresetCombo(inputPreset, numInputChannels, inputChannel,
+ numOutputChannels, outputChannel, false);
+ }
+
+ void testPresetCombo(int inputPreset) throws InterruptedException {
+ testPresetCombo(inputPreset, 1, 0, 1, 0);
+ }
+
+ private void testInputPresets() throws InterruptedException {
+ logBoth("\nTest InputPreset -------");
+
+ for (int inputPreset : INPUT_PRESETS) {
+ testPresetCombo(inputPreset);
+ }
+// TODO Resolve issue with echo cancellation killing the signal.
+// testPresetCombo(StreamConfiguration.INPUT_PRESET_VOICE_COMMUNICATION,
+// 1, 0, 2, 0);
+// testPresetCombo(StreamConfiguration.INPUT_PRESET_VOICE_COMMUNICATION,
+// 1, 0, 2, 1);
+// testPresetCombo(StreamConfiguration.INPUT_PRESET_VOICE_COMMUNICATION,
+// 2, 0, 2, 0);
+// testPresetCombo(StreamConfiguration.INPUT_PRESET_VOICE_COMMUNICATION,
+// 2, 0, 2, 1);
+ }
+
+ void testInputDeviceCombo(int deviceId,
+ int numInputChannels,
+ int inputChannel,
+ boolean mmapEnabled) throws InterruptedException {
+ final int numOutputChannels = 2;
+ setupDeviceCombo(numInputChannels, inputChannel, numOutputChannels, 0);
+
+ StreamConfiguration requestedInConfig = mAudioInputTester.requestedConfiguration;
+ requestedInConfig.setInputPreset(StreamConfiguration.INPUT_PRESET_VOICE_RECOGNITION);
+ requestedInConfig.setDeviceId(deviceId);
+ requestedInConfig.setMMap(mmapEnabled);
+
+ mMagnitude = -1.0;
+ int result = testConfigurations();
+ if (result != TEST_RESULT_SKIPPED) {
+ appendSummary(getOneLineSummary() + "\n");
+ }
+ }
+
+ void testInputDeviceCombo(int deviceId,
+ int numInputChannels,
+ int inputChannel) throws InterruptedException {
+ if (NativeEngine.isMMapSupported()) {
+ testInputDeviceCombo(deviceId, numInputChannels, inputChannel, true);
+ }
+ testInputDeviceCombo(deviceId, numInputChannels, inputChannel, false);
+ }
+
+ void testInputDevices() throws InterruptedException {
+ logBoth("\nTest Input Devices -------");
+
+ AudioDeviceInfo[] devices = mAudioManager.getDevices(AudioManager.GET_DEVICES_INPUTS);
+ int numTested = 0;
+ for (AudioDeviceInfo deviceInfo : devices) {
+ log("----\n"
+ + AudioDeviceInfoConverter.toString(deviceInfo) + "\n");
+ if (!deviceInfo.isSource()) continue; // FIXME log as error?!
+ if (deviceInfo.getType() == AudioDeviceInfo.TYPE_BUILTIN_MIC) {
+ int id = deviceInfo.getId();
+ int[] channelCounts = deviceInfo.getChannelCounts();
+ numTested++;
+ // Always test mono and stereo.
+ testInputDeviceCombo(id, 1, 0);
+ testInputDeviceCombo(id, 2, 0);
+ testInputDeviceCombo(id, 2, 1);
+ 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, numChannels, channel);
+ }
+ }
+ }
+ }
+ } else {
+ log("Device skipped for type.");
+ }
+ }
+
+ if (numTested == 0) {
+ log("NO INPUT DEVICE FOUND!\n");
+ }
+ }
+
+ void testOutputDeviceCombo(int deviceId,
+ int deviceType,
+ int numOutputChannels,
+ int outputChannel,
+ boolean mmapEnabled) throws InterruptedException {
+ final int numInputChannels = 2; // TODO review, done because of mono problems on some devices
+ setupDeviceCombo(numInputChannels, 0, numOutputChannels, outputChannel);
+
+ StreamConfiguration requestedOutConfig = mAudioOutTester.requestedConfiguration;
+ requestedOutConfig.setDeviceId(deviceId);
+ requestedOutConfig.setMMap(mmapEnabled);
+
+ mMagnitude = -1.0;
+ int result = testConfigurations();
+ if (result != TEST_RESULT_SKIPPED) {
+ appendSummary(getOneLineSummary() + "\n");
+ if (result == TEST_RESULT_FAILED) {
+ if (deviceType == AudioDeviceInfo.TYPE_BUILTIN_EARPIECE
+ && numOutputChannels == 2
+ && outputChannel == 1) {
+ logFailed("Maybe EARPIECE does not mix stereo to mono!");
+ }
+ if (deviceType == TYPE_BUILTIN_SPEAKER_SAFE
+ && numOutputChannels == 2
+ && outputChannel == 0) {
+ logFailed("Maybe SPEAKER_SAFE blocked channel 0!");
+ }
+ }
+ }
+ }
+
+ void testOutputDeviceCombo(int deviceId,
+ int deviceType,
+ int numOutputChannels,
+ int outputChannel) throws InterruptedException {
+ if (NativeEngine.isMMapSupported()) {
+ testOutputDeviceCombo(deviceId, deviceType, numOutputChannels, outputChannel, true);
+ }
+ testOutputDeviceCombo(deviceId, deviceType, numOutputChannels, outputChannel, false);
+ }
+
+ void logBoth(String text) {
+ log(text);
+ appendSummary(text + "\n");
+ }
+ void logFailed(String text) {
+ log(text);
+ appendFailedSummary(text + "\n");
+ }
+
+ void testOutputDevices() throws InterruptedException {
+ logBoth("\nTest Output Devices -------");
+
+ AudioDeviceInfo[] devices = mAudioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS);
+ int numTested = 0;
+ for (AudioDeviceInfo deviceInfo : devices) {
+ log("----\n"
+ + AudioDeviceInfoConverter.toString(deviceInfo) + "\n");
+ if (!deviceInfo.isSink()) continue;
+ int deviceType = deviceInfo.getType();
+ if (deviceType == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER
+ || deviceType == AudioDeviceInfo.TYPE_BUILTIN_EARPIECE
+ || deviceType == TYPE_BUILTIN_SPEAKER_SAFE) {
+ int id = deviceInfo.getId();
+ int[] channelCounts = deviceInfo.getChannelCounts();
+ numTested++;
+ // Always test mono and stereo.
+ testOutputDeviceCombo(id, deviceType, 1, 0);
+ testOutputDeviceCombo(id, deviceType, 2, 0);
+ testOutputDeviceCombo(id, deviceType, 2, 1);
+ 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, channel);
+ }
+ }
+ }
+ }
+ } else {
+ log("Device skipped for type.");
+ }
+ }
+ if (numTested == 0) {
+ log("NO OUTPUT DEVICE FOUND!\n");
+ }
+ }
+
+ @Override
+ public void runTest() {
+ try {
+ mDurationSeconds = DURATION_SECONDS;
+
+ log("min.required.magnitude = " + MIN_REQUIRED_MAGNITUDE);
+ log("max.allowed.jitter = " + MAX_ALLOWED_JITTER);
+ log("test.gap.msec = " + mGapMillis);
+
+ testInputPresets();
+ testInputDevices();
+ testOutputDevices();
+ } catch (Exception e) {
+ log(e.getMessage());
+ showErrorToast(e.getMessage());
+ }
+ }
+
+}
diff --git a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestDisconnectActivity.java b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestDisconnectActivity.java
index ce9a5f40..31208c7d 100644
--- a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestDisconnectActivity.java
+++ b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestDisconnectActivity.java
@@ -20,21 +20,18 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
-import android.os.Build;
import android.os.Bundle;
-import android.text.method.ScrollingMovementMethod;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import java.io.IOException;
-import java.util.Date;
/**
* Guide the user through a series of tests plugging in and unplugging a headset.
* Print a summary at the end of any failures.
*/
-public class TestDisconnectActivity extends TestAudioActivity implements Runnable {
+public class TestDisconnectActivity extends TestAudioActivity {
private static final String TEXT_SKIP = "SKIP";
private static final String TEXT_PASS = "PASS";
@@ -44,26 +41,18 @@ public class TestDisconnectActivity extends TestAudioActivity implements Runnabl
public static final int TIME_TO_FAILURE_MILLIS = 3000;
private TextView mInstructionsTextView;
- private TextView mAutoTextView;
private TextView mStatusTextView;
private TextView mPlugTextView;
- private Thread mAutoThread;
- private volatile boolean mThreadEnabled;
private volatile boolean mTestFailed;
private volatile boolean mSkipTest;
private volatile int mPlugCount;
- private int mTestCount;
- private StringBuffer mFailedSummary;
- private int mPassCount;
- private int mFailCount;
private BroadcastReceiver mPluginReceiver = new PluginBroadcastReceiver();
- private Button mStartButton;
- private Button mStopButton;
- private Button mShareButton;
private Button mFailButton;
private Button mSkipButton;
+ protected AutomatedTestRunner mAutomatedTestRunner;
+
// Receive a broadcast Intent when a headset is plugged in or unplugged.
// Display a count on screen.
public class PluginBroadcastReceiver extends BroadcastReceiver {
@@ -89,31 +78,25 @@ public class TestDisconnectActivity extends TestAudioActivity implements Runnabl
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+ mAutomatedTestRunner = findViewById(R.id.auto_test_runner);
+ mAutomatedTestRunner.setActivity(this);
+
mInstructionsTextView = (TextView) findViewById(R.id.text_instructions);
mStatusTextView = (TextView) findViewById(R.id.text_status);
mPlugTextView = (TextView) findViewById(R.id.text_plug_events);
- mAutoTextView = (TextView) findViewById(R.id.text_log);
- mAutoTextView.setMovementMethod(new ScrollingMovementMethod());
- mStartButton = (Button) findViewById(R.id.button_start);
- mStopButton = (Button) findViewById(R.id.button_stop);
- mShareButton = (Button) findViewById(R.id.button_share);
- mShareButton.setEnabled(false);
mFailButton = (Button) findViewById(R.id.button_fail);
mSkipButton = (Button) findViewById(R.id.button_skip);
- updateStartStopButtons(false);
updateFailSkipButton(false);
}
- private void updateStartStopButtons(boolean running) {
- mStartButton.setEnabled(!running);
- mStopButton.setEnabled(running);
+ @Override
+ public String getTestName() {
+ return "Disconnect";
}
- @Override
- protected void onStart() {
- super.onStart();
- setActivityType(ACTIVITY_TEST_DISCONNECT);
+ int getActivityType() {
+ return ACTIVITY_TEST_DISCONNECT;
}
@Override
@@ -135,17 +118,6 @@ public class TestDisconnectActivity extends TestAudioActivity implements Runnabl
});
}
- // Write to scrollable TextView
- private void log(final String text) {
- runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mAutoTextView.append(text);
- mAutoTextView.append("\n");
- }
- });
- }
-
// Write to status and command view
private void setInstructionsText(final String text) {
runOnUiThread(new Runnable() {
@@ -166,15 +138,6 @@ public class TestDisconnectActivity extends TestAudioActivity implements Runnabl
});
}
- private void logClear() {
- runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mAutoTextView.setText("");
- }
- });
- }
-
@Override
public void onResume() {
super.onResume();
@@ -188,14 +151,13 @@ public class TestDisconnectActivity extends TestAudioActivity implements Runnabl
super.onPause();
}
- // Only call from UI thread.
- public void onTestFinished() {
- updateStartStopButtons(false);
- mShareButton.setEnabled(true);
+ // This should only be called from UI events such as onStop or a button press.
+ @Override
+ public void onStopTest() {
+ mAutomatedTestRunner.stopTest();
}
public void startAudioTest() throws IOException {
- openAudio();
startAudio();
}
@@ -206,36 +168,16 @@ public class TestDisconnectActivity extends TestAudioActivity implements Runnabl
public void onCancel(View view) {
stopAudioTest();
- onTestFinished();
+ mAutomatedTestRunner.onTestFinished();
}
// Called on UI thread
public void onStopAudioTest(View view) {
stopAudioTest();
- onTestFinished();
+ mAutomatedTestRunner.onTestFinished();
keepScreenOn(false);
}
- public void onStartDisconnectTest(View view) {
- updateStartStopButtons(true);
- mThreadEnabled = true;
- mAutoThread = new Thread(this);
- mAutoThread.start();
- }
-
- public void onStopDisconnectTest(View view) {
- try {
- if (mAutoThread != null) {
- mThreadEnabled = false;
- mAutoThread.interrupt();
- mAutoThread.join(100);
- mAutoThread = null;
- }
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
-
public void onFailTest(View view) {
mTestFailed = true;
}
@@ -244,20 +186,6 @@ public class TestDisconnectActivity extends TestAudioActivity implements Runnabl
mSkipTest = true;
}
- // Share text from log via GMail, Drive or other method.
- public void onShareResult(View view) {
- Intent sharingIntent = new Intent(Intent.ACTION_SEND);
- sharingIntent.setType("text/plain");
-
- String subjectText = "OboeTester Test Disconnect result " + getTimestampString();
- sharingIntent.putExtra(Intent.EXTRA_SUBJECT, subjectText);
-
- String shareBody = mAutoTextView.getText().toString();
- sharingIntent.putExtra(Intent.EXTRA_TEXT, shareBody);
-
- startActivity(Intent.createChooser(sharingIntent, "Share using:"));
- }
-
private String getConfigText(StreamConfiguration config) {
return ((config.getDirection() == StreamConfiguration.DIRECTION_OUTPUT) ? "OUT" : "IN")
+ ", Perf = " + StreamConfiguration.convertPerformanceModeToText(
@@ -266,6 +194,14 @@ public class TestDisconnectActivity extends TestAudioActivity implements Runnabl
+ ", " + config.getSampleRate();
}
+ private void log(String text) {
+ mAutomatedTestRunner.log(text);
+ }
+
+ private void appendFailedSummary(String text) {
+ mAutomatedTestRunner.appendFailedSummary(text);
+ }
+
private void testConfiguration(boolean isInput,
int perfMode,
int sharingMode,
@@ -301,17 +237,17 @@ public class TestDisconnectActivity extends TestAudioActivity implements Runnabl
requestedConfig.setRateConversionQuality(StreamConfiguration.RATE_CONVERSION_QUALITY_MEDIUM);
}
- log("========================== #" + mTestCount);
+ log("========================== #" + mAutomatedTestRunner.getTestCount());
log("Requested:");
log(getConfigText(requestedConfig));
// Give previous stream time to close and release resources. Avoid race conditions.
Thread.sleep(SETTLING_TIME_MILLIS);
- if (!mThreadEnabled) return;
+ if (!mAutomatedTestRunner.isThreadEnabled()) return;
boolean openFailed = false;
AudioStreamBase stream = null;
try {
- startAudioTest(); // this will fill in actualConfig
+ openAudio();
log("Actual:");
actualConfigText = getConfigText(actualConfig)
+ ", " + (actualConfig.isMMap() ? "MMAP" : "Legacy");
@@ -342,12 +278,22 @@ public class TestDisconnectActivity extends TestAudioActivity implements Runnabl
}
}
+ if (!openFailed && valid) {
+ try {
+ startAudioTest();
+ } catch (IOException e) {
+ e.printStackTrace();
+ valid = false;
+ log(e.getMessage());
+ }
+ }
+
int oldPlugCount = mPlugCount;
if (!openFailed && valid) {
mTestFailed = false;
updateFailSkipButton(true);
// poll until stream started
- while (!mTestFailed && mThreadEnabled && !mSkipTest &&
+ while (!mTestFailed && mAutomatedTestRunner.isThreadEnabled() && !mSkipTest &&
stream.getState() == StreamConfiguration.STREAM_STATE_STARTING) {
Thread.sleep(POLL_DURATION_MILLIS);
}
@@ -356,7 +302,7 @@ public class TestDisconnectActivity extends TestAudioActivity implements Runnabl
setInstructionsText(message);
int timeoutCount = 0;
// Wait for Java plug count to change or stream to disconnect.
- while (!mTestFailed && mThreadEnabled && !mSkipTest &&
+ while (!mTestFailed && mAutomatedTestRunner.isThreadEnabled() && !mSkipTest &&
stream.getState() == StreamConfiguration.STREAM_STATE_STARTED) {
Thread.sleep(POLL_DURATION_MILLIS);
if (mPlugCount > oldPlugCount) {
@@ -365,7 +311,7 @@ public class TestDisconnectActivity extends TestAudioActivity implements Runnabl
}
}
// Wait for timeout or stream to disconnect.
- while (!mTestFailed && mThreadEnabled && !mSkipTest && (timeoutCount > 0) &&
+ while (!mTestFailed && mAutomatedTestRunner.isThreadEnabled() && !mSkipTest && (timeoutCount > 0) &&
stream.getState() == StreamConfiguration.STREAM_STATE_STARTED) {
Thread.sleep(POLL_DURATION_MILLIS);
timeoutCount--;
@@ -396,12 +342,10 @@ public class TestDisconnectActivity extends TestAudioActivity implements Runnabl
if (valid) {
if (openFailed) {
- mFailedSummary.append("------ #" + mTestCount);
- mFailedSummary.append("\n");
- mFailedSummary.append(getConfigText(requestedConfig));
- mFailedSummary.append("\n");
- mFailedSummary.append("Open failed!\n");
- mFailCount++;
+ appendFailedSummary("------ #" + mAutomatedTestRunner.getTestCount() + "\n");
+ appendFailedSummary(getConfigText(requestedConfig) + "\n");
+ appendFailedSummary("Open failed!\n");
+ mAutomatedTestRunner.incrementFailCount();
} else {
log("Result:");
boolean passed = !mTestFailed;
@@ -409,17 +353,12 @@ public class TestDisconnectActivity extends TestAudioActivity implements Runnabl
resultText += ", " + (passed ? TEXT_PASS : TEXT_FAIL);
log(resultText);
if (!passed) {
- mFailedSummary.append("------ #" + mTestCount);
- mFailedSummary.append("\n");
- mFailedSummary.append(" ");
- mFailedSummary.append(actualConfigText);
- mFailedSummary.append("\n");
- mFailedSummary.append(" ");
- mFailedSummary.append(resultText);
- mFailedSummary.append("\n");
- mFailCount++;
+ appendFailedSummary("------ #" + mAutomatedTestRunner.getTestCount() + "\n");
+ appendFailedSummary(" " + actualConfigText + "\n");
+ appendFailedSummary(" " + resultText + "\n");
+ mAutomatedTestRunner.incrementFailCount();
} else {
- mPassCount++;
+ mAutomatedTestRunner.incrementPassCount();
}
}
} else {
@@ -427,7 +366,7 @@ public class TestDisconnectActivity extends TestAudioActivity implements Runnabl
}
// Give hardware time to settle between tests.
Thread.sleep(1000);
- mTestCount++;
+ mAutomatedTestRunner.incrementTestCount();
}
private void testConfiguration(boolean isInput, int performanceMode,
@@ -451,49 +390,24 @@ public class TestDisconnectActivity extends TestAudioActivity implements Runnabl
}
@Override
- public void run() {
+ public void runTest() {
mPlugCount = 0;
- logClear();
- log("=== STARTED at " + new Date());
- log(Build.MANUFACTURER + " " + Build.PRODUCT);
- log(Build.DISPLAY);
- mFailedSummary = new StringBuffer();
- mTestCount = 0;
- mPassCount = 0;
- mFailCount = 0;
// Try several different configurations.
try {
testConfiguration(false, StreamConfiguration.PERFORMANCE_MODE_LOW_LATENCY,
StreamConfiguration.SHARING_MODE_EXCLUSIVE, 44100);
testConfiguration(StreamConfiguration.PERFORMANCE_MODE_LOW_LATENCY,
- StreamConfiguration.SHARING_MODE_EXCLUSIVE);
+ StreamConfiguration.SHARING_MODE_EXCLUSIVE);
testConfiguration(StreamConfiguration.PERFORMANCE_MODE_LOW_LATENCY,
- StreamConfiguration.SHARING_MODE_SHARED);
+ StreamConfiguration.SHARING_MODE_SHARED);
testConfiguration(StreamConfiguration.PERFORMANCE_MODE_NONE,
- StreamConfiguration.SHARING_MODE_SHARED);
+ StreamConfiguration.SHARING_MODE_SHARED);
} catch (InterruptedException e) {
- e.printStackTrace();
+ log(e.getMessage());
+ showErrorToast(e.getMessage());
} finally {
- stopAudioTest();
- setInstructionsText("See summary below.");
- setStatusText("Finished.");
- log("\n==== SUMMARY ========");
- if (mFailCount > 0) {
- log(mPassCount + " passed. " + mFailCount + " failed.");
- log("These tests FAILED:");
- log(mFailedSummary.toString());
- } else {
- log("All tests PASSED.");
- }
- log("== FINISHED at " + new Date());
- runOnUiThread(new Runnable() {
- @Override
- public void run() {
- onTestFinished();
- }
- });
+ setInstructionsText("Test completed.");
updateFailSkipButton(false);
}
}
-
}
diff --git a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestInputActivity.java b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestInputActivity.java
index 273ca4ad..bb034964 100644
--- a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestInputActivity.java
+++ b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestInputActivity.java
@@ -28,13 +28,9 @@ import android.support.annotation.NonNull;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.FileProvider;
import android.view.View;
-import android.widget.CheckBox;
import android.widget.RadioButton;
-import android.widget.TextView;
import android.widget.Toast;
-import com.google.sample.oboe.manualtest.R;
-
import java.io.File;
import java.io.IOException;
@@ -88,9 +84,8 @@ public class TestInputActivity extends TestAudioActivity
}
@Override
- protected void onStart() {
- super.onStart();
- setActivityType(ACTIVITY_TEST_INPUT);
+ int getActivityType() {
+ return ACTIVITY_TEST_INPUT;
}
@Override
@@ -218,7 +213,7 @@ public class TestInputActivity extends TestAudioActivity
private File createFileName() {
// Get directory and filename
File dir = getExternalFilesDir(Environment.DIRECTORY_MUSIC);
- return new File(dir, "oboe_" + getWaveTag() + "_" + getTimestampString() + ".wav");
+ return new File(dir, "oboe_" + getWaveTag() + "_" + AutomatedTestRunner.getTimestampString() + ".wav");
}
public void shareWaveFile() {
diff --git a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestOutputActivity.java b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestOutputActivity.java
index f57e4542..3acdb6e8 100644
--- a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestOutputActivity.java
+++ b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestOutputActivity.java
@@ -76,9 +76,8 @@ public final class TestOutputActivity extends TestOutputActivityBase {
}
@Override
- protected void onStart() {
- super.onStart();
- setActivityType(ACTIVITY_TEST_OUTPUT);
+ int getActivityType() {
+ return ACTIVITY_TEST_OUTPUT;
}
public void openAudio() throws IOException {
diff --git a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestOutputActivityBase.java b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestOutputActivityBase.java
index 8cfeb073..f7578380 100644
--- a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestOutputActivityBase.java
+++ b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestOutputActivityBase.java
@@ -48,7 +48,6 @@ abstract class TestOutputActivityBase extends TestAudioActivity {
@Override
public AudioOutputTester addAudioOutputTester() {
AudioOutputTester audioOutTester = super.addAudioOutputTester();
- mBufferSizeView.setAudioOutTester(audioOutTester);
mWorkloadView.setAudioStreamTester(audioOutTester);
return audioOutTester;
}
@@ -57,7 +56,7 @@ abstract class TestOutputActivityBase extends TestAudioActivity {
public void openAudio() throws IOException {
super.openAudio();
if (mBufferSizeView != null) {
- mBufferSizeView.updateBufferSize();
+ mBufferSizeView.onStreamOpened((OboeAudioStream) mAudioOutTester.getCurrentAudioStream());
}
}
diff --git a/apps/OboeTester/app/src/main/res/layout/activity_auto_glitches.xml b/apps/OboeTester/app/src/main/res/layout/activity_auto_glitches.xml
index 3aea6733..a0176680 100644
--- a/apps/OboeTester/app/src/main/res/layout/activity_auto_glitches.xml
+++ b/apps/OboeTester/app/src/main/res/layout/activity_auto_glitches.xml
@@ -8,7 +8,7 @@
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
- tools:context="com.google.sample.oboe.manualtest.AutoGlitchActivity">
+ tools:context="com.google.sample.oboe.manualtest.AutomatedGlitchActivity">
<LinearLayout
android:layout_width="match_parent"
@@ -33,54 +33,10 @@
/>
</LinearLayout>
- <LinearLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:orientation="horizontal">
-
- <Button
- android:id="@+id/button_start"
- android:layout_width="0dp"
- android:layout_weight="1"
- android:layout_height="wrap_content"
- android:onClick="onStartAudioTest"
- android:text="@string/startAudio" />
-
- <Button
- android:id="@+id/button_stop"
- android:layout_width="0dp"
- android:layout_weight="1"
- android:layout_height="wrap_content"
- android:onClick="onStopAudioTest"
- android:text="@string/stopAudio" />
-
- <Button
- android:id="@+id/button_share"
- android:layout_width="0dp"
- android:layout_weight="1"
- android:layout_height="wrap_content"
- android:onClick="onShareResult"
- android:text="@string/share" />
- </LinearLayout>
-
-
- <TextView
- android:id="@+id/text_status"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:lines="12"
- android:text="@string/auto_glitch_instructions"
- android:textSize="18sp"
- android:textStyle="bold"
- />
-
- <TextView
- android:id="@+id/text_log"
+ <com.google.sample.oboe.manualtest.AutomatedTestRunner
+ android:id="@+id/auto_test_runner"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:scrollbars = "vertical"
- android:gravity="bottom"
- android:text="@string/log_of_glitch_tests"
- />
+ android:orientation="vertical" />
</LinearLayout>
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
new file mode 100644
index 00000000..deb6a3c5
--- /dev/null
+++ b/apps/OboeTester/app/src/main/res/layout/activity_data_paths.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:paddingBottom="@dimen/activity_vertical_margin"
+ android:paddingLeft="@dimen/activity_horizontal_margin"
+ android:paddingRight="@dimen/activity_horizontal_margin"
+ android:paddingTop="@dimen/activity_vertical_margin"
+ tools:context="com.google.sample.oboe.manualtest.TestDataPathsActivity">
+
+ <TextView
+ android:id="@+id/text_instructions"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:lines="1"
+ android:text="@string/test_disconnect_instructions"
+ android:textColor="#F44336"
+ android:textSize="18sp"
+ android:textStyle="bold" />
+
+ <com.google.sample.oboe.manualtest.AutomatedTestRunner
+ android:id="@+id/auto_test_runner"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical" />
+
+</LinearLayout>
diff --git a/apps/OboeTester/app/src/main/res/layout/activity_device_report.xml b/apps/OboeTester/app/src/main/res/layout/activity_device_report.xml
index e76bf69e..8815ae90 100644
--- a/apps/OboeTester/app/src/main/res/layout/activity_device_report.xml
+++ b/apps/OboeTester/app/src/main/res/layout/activity_device_report.xml
@@ -17,6 +17,7 @@
android:fontFamily="monospace"
android:gravity="bottom"
android:scrollbars="vertical"
- android:text="@string/device_report" />
+ android:text="@string/device_report"
+ />
</LinearLayout>
diff --git a/apps/OboeTester/app/src/main/res/layout/activity_echo.xml b/apps/OboeTester/app/src/main/res/layout/activity_echo.xml
index bed09c76..34bb49db 100644
--- a/apps/OboeTester/app/src/main/res/layout/activity_echo.xml
+++ b/apps/OboeTester/app/src/main/res/layout/activity_echo.xml
@@ -76,5 +76,15 @@
android:progress="1000" />
</LinearLayout>
+
+ <TextView
+ android:id="@+id/text_status"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:lines="11"
+ android:text="@string/echo_instructions"
+ android:textSize="14sp"
+ android:textStyle="bold" />
+
</LinearLayout>
</ScrollView> \ No newline at end of file
diff --git a/apps/OboeTester/app/src/main/res/layout/activity_main.xml b/apps/OboeTester/app/src/main/res/layout/activity_main.xml
index 5d0cce25..248f5728 100644
--- a/apps/OboeTester/app/src/main/res/layout/activity_main.xml
+++ b/apps/OboeTester/app/src/main/res/layout/activity_main.xml
@@ -119,8 +119,18 @@
android:onClick="onLaunchTestDeviceReport"
android:text="@string/title_report_devices" />
+ <Button
+ android:id="@+id/button_test_data_paths"
+ android:layout_gravity="fill"
+ android:layout_width="0dp"
+ android:layout_columnWeight="1"
+ android:layout_height="wrap_content"
+ android:onClick="onLaunchTestDataPaths"
+ android:text="Data Paths" />
+
</GridLayout>
+
<CheckBox
android:id="@+id/useCallback"
android:layout_width="wrap_content"
diff --git a/apps/OboeTester/app/src/main/res/layout/activity_manual_glitches.xml b/apps/OboeTester/app/src/main/res/layout/activity_manual_glitches.xml
index c2ab5a22..fb091418 100644
--- a/apps/OboeTester/app/src/main/res/layout/activity_manual_glitches.xml
+++ b/apps/OboeTester/app/src/main/res/layout/activity_manual_glitches.xml
@@ -93,7 +93,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:lines="11"
- android:text="@string/use_loopback"
+ android:text="@string/loopback_instructions_glitch"
android:textSize="14sp"
android:textStyle="bold" />
diff --git a/apps/OboeTester/app/src/main/res/layout/activity_rt_latency.xml b/apps/OboeTester/app/src/main/res/layout/activity_rt_latency.xml
index de3ca261..7780985c 100644
--- a/apps/OboeTester/app/src/main/res/layout/activity_rt_latency.xml
+++ b/apps/OboeTester/app/src/main/res/layout/activity_rt_latency.xml
@@ -52,10 +52,11 @@
<Button
android:id="@+id/button_measure"
android:layout_width="0dp"
- android:layout_weight="1"
android:layout_height="wrap_content"
+ android:layout_weight="1"
android:onClick="onMeasure"
- android:text="@string/measure" />
+ android:text="@string/measure"
+ android:textSize="12sp" />
<Button
android:id="@+id/button_average"
@@ -63,7 +64,8 @@
android:layout_weight="1"
android:layout_height="wrap_content"
android:onClick="onAverage"
- android:text="@string/average" />
+ android:text="@string/average"
+ android:textSize="12sp" />
<Button
android:id="@+id/button_cancel"
@@ -72,7 +74,8 @@
android:layout_height="wrap_content"
android:enabled="false"
android:onClick="onCancel"
- android:text="@string/cancel" />
+ android:text="@string/cancel"
+ android:textSize="12sp" />
<Button
android:id="@+id/button_share"
@@ -81,7 +84,7 @@
android:layout_height="wrap_content"
android:onClick="onShareFile"
android:text="@string/share"
- />
+ android:textSize="12sp" />
</LinearLayout>
<TextView
@@ -89,7 +92,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:lines="16"
- android:text="@string/use_loopback"
+ android:text="@string/loopback_instructions_latency"
android:textSize="18sp"
android:textStyle="bold" />
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 0c745590..fa11eed6 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
@@ -10,37 +10,6 @@
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="com.google.sample.oboe.manualtest.TestDisconnectActivity">
- <LinearLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:orientation="horizontal">
-
- <Button
- android:id="@+id/button_start"
- android:layout_width="0dp"
- android:layout_weight="1"
- android:layout_height="wrap_content"
- android:onClick="onStartDisconnectTest"
- android:text="@string/startAudio" />
-
- <Button
- android:id="@+id/button_stop"
- android:layout_width="0dp"
- android:layout_weight="1"
- android:layout_height="wrap_content"
- android:onClick="onStopDisconnectTest"
- android:text="@string/stopAudio" />
-
- <Button
- android:id="@+id/button_share"
- android:layout_width="0dp"
- android:layout_weight="1"
- android:layout_height="wrap_content"
- android:onClick="onShareResult"
- android:text="@string/share" />
- </LinearLayout>
-
-
<TextView
android:id="@+id/text_instructions"
android:layout_width="match_parent"
@@ -52,17 +21,6 @@
android:textStyle="bold" />
<TextView
- android:id="@+id/text_status"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:lines="2"
- android:text="---"
- android:textSize="18sp"
- android:textStyle="bold"
- />
-
-
- <TextView
android:id="@+id/text_plug_events"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -72,6 +30,7 @@
android:textStyle="bold"
/>
+
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -95,13 +54,10 @@
</LinearLayout>
- <TextView
- android:id="@+id/text_log"
+ <com.google.sample.oboe.manualtest.AutomatedTestRunner
+ android:id="@+id/auto_test_runner"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:scrollbars = "vertical"
- android:gravity="bottom"
- android:text="@string/log_of_glitch_tests"
- />
+ android:orientation="vertical" />
</LinearLayout>
diff --git a/apps/OboeTester/app/src/main/res/layout/auto_test_runner.xml b/apps/OboeTester/app/src/main/res/layout/auto_test_runner.xml
new file mode 100644
index 00000000..05e030b9
--- /dev/null
+++ b/apps/OboeTester/app/src/main/res/layout/auto_test_runner.xml
@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <Button
+ android:id="@+id/button_start"
+ android:layout_width="0dp"
+ android:layout_weight="1"
+ android:layout_height="wrap_content"
+ android:text="@string/startAudio" />
+
+ <Button
+ android:id="@+id/button_stop"
+ android:layout_width="0dp"
+ android:layout_weight="1"
+ android:layout_height="wrap_content"
+ android:text="@string/stopAudio" />
+
+ <Button
+ android:id="@+id/button_share"
+ android:layout_width="0dp"
+ android:layout_weight="1"
+ android:layout_height="wrap_content"
+ android:text="@string/share" />
+ </LinearLayout>
+
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+ <TextView
+ android:id="@+id/single_text_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="4dp"
+ android:text="Single Test #"/>
+
+ <EditText
+ android:id="@+id/single_test_index"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:ems="8"
+ android:inputType="number"
+ android:text=""/>
+ </LinearLayout>
+
+ <TextView
+ android:id="@+id/text_status"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minLines="2"
+ android:maxLines="12"
+ android:text="@string/auto_default_status"
+ android:textSize="18sp"
+ android:textStyle="bold"
+ />
+
+ <TextView
+ android:id="@+id/text_log"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:scrollbars = "vertical"
+ android:gravity="bottom"
+ android:text="@string/log_of_test_results"
+ />
+
+</LinearLayout> \ No newline at end of file
diff --git a/apps/OboeTester/app/src/main/res/layout/buffer_size_view.xml b/apps/OboeTester/app/src/main/res/layout/buffer_size_view.xml
index 547112eb..04d4fad6 100644
--- a/apps/OboeTester/app/src/main/res/layout/buffer_size_view.xml
+++ b/apps/OboeTester/app/src/main/res/layout/buffer_size_view.xml
@@ -10,11 +10,44 @@
android:layout_height="wrap_content"
android:text="BufferSIZE" />
+
+ <LinearLayout
+ android:orientation="horizontal"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <RadioGroup
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <RadioButton
+ android:id="@+id/bufferSize1"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="1"
+ />
+
+ <RadioButton
+ android:id="@+id/bufferSize2"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="2"
+ />
+
+ <RadioButton
+ android:id="@+id/bufferSize3"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="3"
+ />
+ </RadioGroup>
<SeekBar
- android:id="@+id/faderThreshold"
+ android:id="@+id/faderBufferSize"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:max="1000"
android:progress="1000" />
+ </LinearLayout>
</LinearLayout> \ No newline at end of file
diff --git a/apps/OboeTester/app/src/main/res/layout/stream_config.xml b/apps/OboeTester/app/src/main/res/layout/stream_config.xml
index 9db517ea..16aab9d3 100644
--- a/apps/OboeTester/app/src/main/res/layout/stream_config.xml
+++ b/apps/OboeTester/app/src/main/res/layout/stream_config.xml
@@ -272,7 +272,7 @@
android:id="@+id/statusView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:lines="3"
+ android:lines="4"
android:text="@string/init_status" />
</LinearLayout>
diff --git a/apps/OboeTester/app/src/main/res/values/strings.xml b/apps/OboeTester/app/src/main/res/values/strings.xml
index 45ded340..6ad15372 100644
--- a/apps/OboeTester/app/src/main/res/values/strings.xml
+++ b/apps/OboeTester/app/src/main/res/values/strings.xml
@@ -19,7 +19,25 @@
<string name="auto_select">Auto select</string>
<string name="hint_hide_settings">Hide Settings</string>
<string name="hint_show_settings">Show Settings</string>
- <string name="use_loopback">Click Show Settings to configure stream.\nRequires a loopback adapter!</string>
+ <string name="loopback_instructions_glitch">
+ Click Show Settings to configure.\n
+ This glitch measurement plays\n
+ and listens to a sine wave.\n
+ It works with best with a loopback dongle.\n
+ Set volume to medium-high.\n
+ </string>
+ <string name="echo_instructions">
+ Be ready to turn down the volume\n
+ if you get feedback. \n
+ </string>
+ <string name="loopback_instructions_latency">
+ Click Show Settings to configure.\n
+ This latency measurement uses a\n
+ short burst of encoded noise.\n
+ It works with speakers and mic\n
+ or a loopback dongle.\n
+ Set volume to medium-high.\n
+ </string>
<string name="api_prompt">Choose an API</string>
<string-array name="output_modes">
@@ -123,23 +141,26 @@
<string name="title_activity_echo">Echo Input to Output</string>
<string name="title_activity_rt_latency">Round Trip Latency</string>
<string name="title_activity_glitches">Test Glitches</string>
+ <string name="title_activity_auto_glitches">Auto Glitches</string>
<string name="title_test_disconnect">Test Disconnect</string>
<string name="title_report_devices">Report Devices</string>
+ <string name="title_data_paths">Data Paths</string>
<string name="need_record_audio_permission">"This app needs RECORD_AUDIO permission"</string>
<string name="share">Share</string>
<string name="seconds_per_test">seconds per test</string>
<string name="duration">Duration:</string>
- <string name="auto_glitch_instructions">Use loopback adapter or speakers.</string>
- <string name="log_of_glitch_tests">Log of Glitch Tests</string>
+ <string name="auto_default_status">Status...</string>
<string name="device_report">Device Report</string>
+ <string name="log_of_test_results">Log of Test Results</string>
<string name="save_file">Save</string>
<string name="src_prompt">SRC:</string>
<string name="average">Average</string>
<string name="failTest">Fail</string>
<string name="skipTest">Skip</string>
- <string name="test_disconnect_instructions">Unplug all headsets then click [START]</string>
+ <string name="test_disconnect_instructions">Unplug headsets, volume up, [START]</string>
+
<string-array name="conversion_qualities">
<item>None</item>
<item>Fastest</item>
diff --git a/apps/OboeTester/docs/Usage.md b/apps/OboeTester/docs/Usage.md
index f079408d..dcedfc6e 100644
--- a/apps/OboeTester/docs/Usage.md
+++ b/apps/OboeTester/docs/Usage.md
@@ -50,7 +50,9 @@ You can then examine the WAV file using a program like Audacity.
This test copies input to output and adds up to 3 seconds of delay.
This can be used to simulate high latency on a phone that supports low latency.
Try putting the phone to your ear with the added delay at 0 and try talking.
-Then set it to about 1-200 msec and try talking on the phone. Notice how the echo can be very confusing.
+Then set it to about 700 msec and try talking on the phone. Notice how the echo can be very confusing.
+
+The test will also display estimated "cold start latency" for full duplex streams.
### Round Trip Latency
@@ -84,5 +86,15 @@ The Share button will let you email the report to yourself.
### Test Disconnect
You can test whether the disconnect logic is working in Android by plugging or unplugging a headset.
-Just follow the instructions in red. You will get a report at the end that you can SHARE by GMail.
+Just follow the instructions in red. You will get a report at the end that you can SHARE by GMail or Drive.
+
+### Data Paths
+
+This checks for dead speaker and mic channels, dead Input Presets and other audio data path problems.
+1. Tap "DATA PATHS" button.
+1. Unplug or disconnect any headphones.
+1. Set volume to medium high.
+1. Place the phone on a table in a quiet room and hit START.
+1. Wait a few minutes, quietly, for the test to complete. You will hear some sine tones.
+1. You will get a report at the end that you can SHARE by GMail or Drive.
diff --git a/apps/fxlab/app/src/main/java/com/mobileer/androidfxlab/MainActivity.kt b/apps/fxlab/app/src/main/java/com/mobileer/androidfxlab/MainActivity.kt
index 3370edb3..1873d1cf 100644
--- a/apps/fxlab/app/src/main/java/com/mobileer/androidfxlab/MainActivity.kt
+++ b/apps/fxlab/app/src/main/java/com/mobileer/androidfxlab/MainActivity.kt
@@ -98,9 +98,25 @@ class MainActivity : AppCompatActivity() {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) {
handleMidiDevices()
}
+
+ }
+
+ override fun onDestroy() {
+ // Clear the FX UI
+ EffectsAdapter.effectList.clear()
+ EffectsAdapter.notifyDataSetChanged()
+ super.onDestroy()
+ }
+
+ override fun onPause() {
+ // Shutdown Engine
+ NativeInterface.destroyAudioEngine()
+ super.onPause()
}
+
override fun onResume() {
super.onResume()
+ // Startup Engine
if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO)
== PackageManager.PERMISSION_GRANTED
) {
@@ -109,13 +125,6 @@ class MainActivity : AppCompatActivity() {
}
}
- override fun onPause() {
- NativeInterface.destroyAudioEngine()
- EffectsAdapter.effectList.clear()
- EffectsAdapter.notifyDataSetChanged()
- super.onPause()
- }
-
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
diff --git a/apps/fxlab/build.gradle b/apps/fxlab/build.gradle
index d8b710c9..57266f67 100644
--- a/apps/fxlab/build.gradle
+++ b/apps/fxlab/build.gradle
@@ -24,7 +24,7 @@ buildscript {
}
dependencies {
- classpath 'com.android.tools.build:gradle:3.5.2'
+ classpath 'com.android.tools.build:gradle:4.2.0-beta02'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
diff --git a/apps/fxlab/gradle/wrapper/gradle-wrapper.properties b/apps/fxlab/gradle/wrapper/gradle-wrapper.properties
index 84fc8bc4..1a55b24e 100644
--- a/apps/fxlab/gradle/wrapper/gradle-wrapper.properties
+++ b/apps/fxlab/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip
+distributionUrl=https://services.gradle.org/distributions/gradle-6.7.1-all.zip
diff --git a/docs/AppsUsingOboe.md b/docs/AppsUsingOboe.md
index a3e67f80..1b2c0339 100644
--- a/docs/AppsUsingOboe.md
+++ b/docs/AppsUsingOboe.md
@@ -6,6 +6,7 @@ your project name and Play Store URL.
| Name | Description | Publisher | Notes |
|:--|:--|:--|:--|
+| [4Beats](https://play.google.com/store/apps/details?id=com.fourbeats) | Easy song creation tool | Code sign | 44100 Hz so no MMAP |
| [Audio Evolution Mobile Studio](https://play.google.com/store/apps/details?id=com.extreamsd.aemobile) | Digital Audio Workstation app | eXtream Software Development | Oboe implemented in version 4.9.8.1 |
| [AudioLab - Audio Editor Recorder & Ringtone Maker](https://play.google.com/store/apps/details?id=com.hitrolab.audioeditor) | THE ONLY AUDIO EDITOR APP YOU WILL EVER NEED | HitroLab | Using Oboe in AudioLab Karaoke offline feature to record audio with the lowest latency |
| BeatScratch [Free](https://play.google.com/store/apps/details?id=com.jonlatane.beatpad.free), [Pro](https://play.google.com/store/apps/details?id=com.jonlatane.beatpad) | 3-instrument MIDI controller, sequencer, and 5-part musical notepad | Jon Latané | Using FluidSynth's Oboe Driver |
diff --git a/docs/FullGuide.md b/docs/FullGuide.md
index 931b67f7..d928a1fb 100644
--- a/docs/FullGuide.md
+++ b/docs/FullGuide.md
@@ -361,13 +361,24 @@ Your code can access the callback mechanism by implementing the virtual class
`AudioStreamDataCallback`. The stream periodically executes `onAudioReady()` (the
callback function) to acquire the data for its next burst.
+The total number of samples that you need to fill is numFrames * numChannels.
+
class AudioEngine : AudioStreamDataCallback {
public:
DataCallbackResult AudioEngine::onAudioReady(
AudioStream *oboeStream,
void *audioData,
int32_t numFrames){
- oscillator_->render(static_cast<float *>(audioData), numFrames);
+ // Fill the output buffer with random white noise.
+ const int numChannels = AAudioStream_getChannelCount(stream);
+ // This code assumes the format is AAUDIO_FORMAT_PCM_FLOAT.
+ float *output = (float *)audioData;
+ for (int frameIndex = 0; frameIndex < numFrames; frameIndex++) {
+ for (int channelIndex = 0; channelIndex < numChannels; channelIndex++) {
+ float noise = (float)(drand48() - 0.5);
+ *output++ = noise;
+ }
+ }
return DataCallbackResult::Continue;
}
@@ -377,14 +388,12 @@ callback function) to acquire the data for its next burst.
streamBuilder.setDataCallback(this);
}
private:
- // application data
- Oscillator* oscillator_;
+ // application data goes here
}
Note that the callback must be registered on the stream with `setDataCallback`. Any
-application-specific data (such as `oscillator_` in this case)
-can be included within the class itself.
+application-specific data can be included within the class itself.
The callback function should not perform a read or write on the stream that invoked it. If the callback belongs to an input stream, your code should process the data that is supplied in the audioData buffer (specified as the second argument). If the callback belongs to an output stream, your code should place data into the buffer.
diff --git a/docs/GettingStarted.md b/docs/GettingStarted.md
index 2c322140..dd8915ea 100644
--- a/docs/GettingStarted.md
+++ b/docs/GettingStarted.md
@@ -1,14 +1,13 @@
# Adding Oboe to your project
There are two ways use Oboe in your Android Studio project:
-1) **Use the Oboe pre-built library binaries and headers** *(Experimental)*. Use this approach if you just want to use a stable version of the Oboe library in your project.
+1) **Use the Oboe pre-built library binaries and headers**. Use this approach if you just want to use a stable version of the Oboe library in your project.
or
2) **Build Oboe from source.** Use this approach if you would like to debug or make changes to the Oboe source code and contribute back to the project.
## Option 1) Using pre-built binaries and headers
-*This approach is currently experimental as it uses a preview version of Android Studio.*
Oboe is distributed as a [prefab](https://github.com/google/prefab) package via [Google Maven](https://maven.google.com/web/index.html) (search for "oboe"). [Prefab support was added](https://android-developers.googleblog.com/2020/02/native-dependencies-in-android-studio-40.html) to [Android Studio Preview 4.0 Canary 9](https://developer.android.com/studio/preview) so you'll need to be using this version of Android Studio or above.
@@ -185,13 +184,13 @@ Supply this callback class to the builder:
builder.setDataCallback(&myCallback);
-Declare a ManagedStream. Make sure it is declared in an appropriate scope (e.g.the member of a managing class). Avoid declaring it as a global.
+Declare a shared pointer for the stream. Make sure it is declared in an appropriate scope (e.g.the member of a managing class). Avoid declaring it as a global.
```
-oboe::ManagedStream managedStream;
+std::shared_ptr<oboe::AudioStream> mStream;
```
Open the stream:
- oboe::Result result = builder.openManagedStream(managedStream);
+ oboe::Result result = builder.openStream(mStream);
Check the result to make sure the stream was opened successfully. Oboe has a convenience method for converting its types into human-readable strings called `oboe::convertToText`:
@@ -205,63 +204,46 @@ Note that this sample code uses the [logging macros from here](https://github.co
Check the properties of the created stream. If you did not specify a channelCount, sampleRate, or format then you need to
query the stream to see what you got. The **format** property will dictate the `audioData` type in the `AudioStreamDataCallback::onAudioReady` callback. If you did specify any of those three properties then you will get what you requested.
- oboe::AudioFormat format = stream->getFormat();
+ oboe::AudioFormat format = mStream->getFormat();
LOGI("AudioStream format is %s", oboe::convertToText(format));
Now start the stream.
- managedStream->requestStart();
+ mStream->requestStart();
At this point you should start receiving callbacks.
To stop receiving callbacks call
- managedStream->requestStop();
+ mStream->requestStop();
## Closing the stream
It is important to close your stream when you're not using it to avoid hogging audio resources which other apps could use. This is particularly true when using `SharingMode::Exclusive` because you might prevent other apps from obtaining a low latency audio stream.
-Streams can be explicitly closed:
+Streams should be explicitly closed when the app is no longer playing audio.
- stream->close();
+ mStream->close();
`close()` is a blocking call which also stops the stream.
-Streams can also be automatically closed when going out of scope:
-
- {
- ManagedStream mStream;
- AudioStreamBuilder().build(mStream);
- mStream->requestStart();
- } // Out of this scope the mStream has been automatically closed
-
-It is preferable to let the `ManagedStream` object go out of scope (or be explicitly deleted) when the app is no longer playing audio.
For apps which only play or record audio when they are in the foreground this is usually done when [`Activity.onPause()`](https://developer.android.com/guide/components/activities/activity-lifecycle#onpause) is called.
## Reconfiguring streams
-In order to change the configuration of the stream, simply call `openManagedStream`
-again. The existing stream is closed, destroyed and a new stream is built and
-populates the `managedStream`.
+After closing, in order to change the configuration of the stream, simply call `openStream`
+again. The existing stream is deleted and a new stream is built and
+populates the `mStream` variable.
```
// Modify the builder with some additional properties at runtime.
builder.setDeviceId(MY_DEVICE_ID);
// Re-open the stream with some additional config
-// The old ManagedStream is automatically closed and deleted
-builder.openManagedStream(managedStream);
+// The old AudioStream is automatically deleted
+builder.openStream(mStream);
```
-The `ManagedStream` takes care of its own closure and destruction. If used in an
-automatic allocation context (such as a member of a class), the stream does not
-need to be closed or deleted manually. Make sure that the object which is responsible for
-the `ManagedStream` (its enclosing class) goes out of scope whenever the app is no longer
-playing or recording audio, such as when `Activity.onPause()` is called.
-
## Example
-The following class is a complete implementation of a `ManagedStream`, which
-renders a sine wave. Creating the class (e.g. through the JNI bridge) creates
-and opens an Oboe stream which renders audio, and its destruction stops and
-closes the stream.
+The following class is a complete implementation of an audio player that
+renders a sine wave.
```
#include <oboe/Oboe.h>
#include <math.h>
@@ -269,19 +251,37 @@ closes the stream.
class OboeSinePlayer: public oboe::AudioStreamDataCallback {
public:
+ virtual ~OboeSinePlayer() = default;
- OboeSinePlayer() {
+ // Call this from Activity onResume()
+ int32_t startAudio() {
+ std::lock_guard<std::mutex> lock(mLock);
oboe::AudioStreamBuilder builder;
// The builder set methods can be chained for convenience.
- builder.setSharingMode(oboe::SharingMode::Exclusive)
- ->setPerformanceMode(oboe::PerformanceMode::LowLatency)
- ->setChannelCount(kChannelCount)
- ->setSampleRate(kSampleRate)
- ->setFormat(oboe::AudioFormat::Float)
- ->setDataCallback(this)
- ->openManagedStream(outStream);
+ Result result = builder.setSharingMode(oboe::SharingMode::Exclusive)
+ ->setPerformanceMode(oboe::PerformanceMode::LowLatency)
+ ->setChannelCount(kChannelCount)
+ ->setSampleRate(kSampleRate)
+ ->setSampleRateConversionQuality(oboe::SampleRateConversionQuality::Medium);
+ ->setFormat(oboe::AudioFormat::Float)
+ ->setDataCallback(this)
+ ->openStream(mStream);
+ if (result != Result::OK) return (int32_t) result;
+
// Typically, start the stream after querying some stream information, as well as some input from the user
- outStream->requestStart();
+ result = outStream->requestStart();
+ return (int32_t) result;
+ }
+
+ // Call this from Activity onPause()
+ void stopAudio() {
+ // Stop, close and delete in case not already closed.
+ std::lock_guard<std::mutex> lock(mLock);
+ if (mStream) {
+ mStream->stop();
+ mStream->close();
+ mStream.reset();
+ }
}
oboe::DataCallbackResult onAudioReady(oboe::AudioStream *oboeStream, void *audioData, int32_t numFrames) override {
@@ -298,7 +298,9 @@ public:
}
private:
- oboe::ManagedStream outStream;
+ std::mutex mLock;
+ std::shared_ptr<oboe::AudioStream> mStream;
+
// Stream params
static int constexpr kChannelCount = 2;
static int constexpr kSampleRate = 48000;
@@ -312,14 +314,12 @@ private:
float mPhase = 0.0;
};
```
-Note that this implementation computes sine values at run-time for simplicity,
+Note that this implementation computes sine values at run-time for simplicity,
rather than pre-computing them.
Additionally, best practice is to implement a separate data callback class, rather
than managing the stream and defining its data callback in the same class.
-This class also automatically starts the stream upon construction. Typically,
-the stream is queried for information prior to being started (e.g. burst size),
-and started upon user input.
-For more examples on how to use `ManagedStream` look in the [samples](https://github.com/google/oboe/tree/master/samples) folder.
+
+For more examples on how to use Oboe look in the [samples](https://github.com/google/oboe/tree/master/samples) folder.
## Obtaining optimal latency
One of the goals of the Oboe library is to provide low latency audio streams on the widest range of hardware configurations.
diff --git a/include/oboe/AudioStreamBuilder.h b/include/oboe/AudioStreamBuilder.h
index de9c6069..bd125850 100644
--- a/include/oboe/AudioStreamBuilder.h
+++ b/include/oboe/AudioStreamBuilder.h
@@ -429,7 +429,8 @@ public:
/**
* Create and open a stream object based on the current settings.
*
- * The caller owns the pointer to the AudioStream object.
+ * The caller owns the pointer to the AudioStream object
+ * and must delete it when finished.
*
* @deprecated Use openStream(std::shared_ptr<oboe::AudioStream> &stream) instead.
* @param stream pointer to a variable to receive the stream address
@@ -455,6 +456,8 @@ public:
* The caller must create a unique ptr, and pass by reference so it can be
* modified to point to an opened stream. The caller owns the unique ptr,
* and it will be automatically closed and deleted when going out of scope.
+ *
+ * @deprecated Use openStream(std::shared_ptr<oboe::AudioStream> &stream) instead.
* @param stream Reference to the ManagedStream (uniqueptr) used to keep track of stream
* @return OBOE_OK if successful or a negative error code.
*/
diff --git a/include/oboe/Version.h b/include/oboe/Version.h
index fac42d4c..1fd18054 100644
--- a/include/oboe/Version.h
+++ b/include/oboe/Version.h
@@ -37,7 +37,7 @@
#define OBOE_VERSION_MINOR 5
// 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/samples/LiveEffect/README.md b/samples/LiveEffect/README.md
index f699ccf1..c24292bd 100644
--- a/samples/LiveEffect/README.md
+++ b/samples/LiveEffect/README.md
@@ -15,9 +15,12 @@ Screenshots
- oboe::I16
- stereo or mono
+### Customizing the App
+
+If you want to customize the effects processing then modify the
+onBothStreamsReady() method in "src/main/cpp/FullDuplexPass.h"
### Caveats
When first time starting audio devices, the stream may not be stable.
The symptom is the strange callback pattern. This sample waits half a second
for audio system to stablize. It is an estimate, it would vary on different platforms.
-
diff --git a/samples/LiveEffect/src/main/cpp/FullDuplexPass.h b/samples/LiveEffect/src/main/cpp/FullDuplexPass.h
index 9a250f83..6f5d05ff 100644
--- a/samples/LiveEffect/src/main/cpp/FullDuplexPass.h
+++ b/samples/LiveEffect/src/main/cpp/FullDuplexPass.h
@@ -38,7 +38,7 @@ public:
// It also assumes the channel count for each stream is the same.
int32_t samplesPerFrame = outputStream->getChannelCount();
int32_t numInputSamples = numInputFrames * samplesPerFrame;
- int32_t numOutputSamples = numInputFrames * samplesPerFrame;
+ int32_t numOutputSamples = numOutputFrames * samplesPerFrame;
// It is possible that there may be fewer input than output samples.
int32_t samplesToProcess = std::min(numInputSamples, numOutputSamples);
diff --git a/samples/drumthumper/README.md b/samples/drumthumper/README.md
index 1e658689..d3e679b1 100644
--- a/samples/drumthumper/README.md
+++ b/samples/drumthumper/README.md
@@ -3,7 +3,7 @@
Oboe playback sample app.
## Abstract
-**DrumThumper** (apolgies to [Chumbawamba](https://www.youtube.com/watch?v=2H5uWRjFsGc)) is a "Drum Pad" app which demonstrates best-practices for low-latency audio playback using the Android **Oboe** API.
+**DrumThumper** is a "Drum Pad" app which demonstrates best-practices for low-latency audio playback using the Android **Oboe** API.
**DrumThumper** consists of a set of trigger pad widgets and an optional UI for controlling the level and stereo placement of each of the virtual drums.
The audio samples are stored in application resources as WAV data. This is parsed and loaded (by routines in **parselib**) into memory blocks.
The audio samples are mixed and played by routines in **iolib**.
diff --git a/samples/drumthumper/src/main/res/layout-land/drumthumper_activity.xml b/samples/drumthumper/src/main/res/layout-land/drumthumper_activity.xml
index f27c0e57..b6577387 100644
--- a/samples/drumthumper/src/main/res/layout-land/drumthumper_activity.xml
+++ b/samples/drumthumper/src/main/res/layout-land/drumthumper_activity.xml
@@ -7,14 +7,6 @@
android:orientation="vertical"
tools:context="com.plausiblesoftware.drumthumper.DrumThumperActivity">
- <TextView
- android:id="@+id/textView"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:gravity="center"
- android:text="DrumThumper"
- android:textSize="14pt" />
-
<LinearLayout
android:layout_width="match_parent"
android:layout_height="128dp"
diff --git a/samples/drumthumper/src/main/res/layout/drumthumper_activity.xml b/samples/drumthumper/src/main/res/layout/drumthumper_activity.xml
index 7b799f94..1c0573b5 100644
--- a/samples/drumthumper/src/main/res/layout/drumthumper_activity.xml
+++ b/samples/drumthumper/src/main/res/layout/drumthumper_activity.xml
@@ -7,14 +7,6 @@
android:orientation="vertical"
tools:context="com.plausiblesoftware.drumthumper.DrumThumperActivity">
- <TextView
- android:id="@+id/textView"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:gravity="center"
- android:text="DrumThumper"
- android:textSize="22pt" />
-
<LinearLayout
android:layout_width="match_parent"
android:layout_height="128dp"
diff --git a/src/aaudio/AAudioExtensions.h b/src/aaudio/AAudioExtensions.h
new file mode 100644
index 00000000..51a37ae0
--- /dev/null
+++ b/src/aaudio/AAudioExtensions.h
@@ -0,0 +1,172 @@
+/*
+ * Copyright 2019 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 OBOE_AAUDIO_EXTENSIONS_H
+#define OBOE_AAUDIO_EXTENSIONS_H
+
+#include <dlfcn.h>
+#include <stdint.h>
+
+#include <sys/system_properties.h>
+
+#include "common/OboeDebug.h"
+#include "oboe/Oboe.h"
+#include "AAudioLoader.h"
+
+namespace oboe {
+
+#define LIB_AAUDIO_NAME "libaaudio.so"
+#define FUNCTION_IS_MMAP "AAudioStream_isMMapUsed"
+#define FUNCTION_SET_MMAP_POLICY "AAudio_setMMapPolicy"
+#define FUNCTION_GET_MMAP_POLICY "AAudio_getMMapPolicy"
+
+#define AAUDIO_ERROR_UNAVAILABLE static_cast<aaudio_result_t>(Result::ErrorUnavailable)
+
+typedef struct AAudioStreamStruct AAudioStream;
+
+/**
+ * Call some AAudio test routines that are not part of the normal API.
+ */
+class AAudioExtensions {
+public:
+ AAudioExtensions() {
+ int32_t policy = getIntegerProperty("aaudio.mmap_policy", 0);
+ mMMapSupported = isPolicyEnabled(policy);
+
+ policy = getIntegerProperty("aaudio.mmap_exclusive_policy", 0);
+ mMMapExclusiveSupported = isPolicyEnabled(policy);
+ }
+
+ static bool isPolicyEnabled(int32_t policy) {
+ return (policy == AAUDIO_POLICY_AUTO || policy == AAUDIO_POLICY_ALWAYS);
+ }
+
+ static AAudioExtensions &getInstance() {
+ static AAudioExtensions instance;
+ return instance;
+ }
+
+ bool isMMapUsed(oboe::AudioStream *oboeStream) {
+ AAudioStream *aaudioStream = (AAudioStream *) oboeStream->getUnderlyingStream();
+ return isMMapUsed(aaudioStream);
+ }
+
+ bool isMMapUsed(AAudioStream *aaudioStream) {
+ if (loadSymbols()) return false;
+ if (mAAudioStream_isMMap == nullptr) return false;
+ return mAAudioStream_isMMap(aaudioStream);
+ }
+
+ /**
+ * Controls whether the MMAP data path can be selected when opening a stream.
+ * It has no effect after the stream has been opened.
+ * It only affects the application that calls it. Other apps are not affected.
+ *
+ * @param enabled
+ * @return 0 or a negative error code
+ */
+ int32_t setMMapEnabled(bool enabled) {
+ if (loadSymbols()) return AAUDIO_ERROR_UNAVAILABLE;
+ if (mAAudio_setMMapPolicy == nullptr) return false;
+ return mAAudio_setMMapPolicy(enabled ? AAUDIO_POLICY_AUTO : AAUDIO_POLICY_NEVER);
+ }
+
+ bool isMMapEnabled() {
+ if (loadSymbols()) return false;
+ if (mAAudio_getMMapPolicy == nullptr) return false;
+ int32_t policy = mAAudio_getMMapPolicy();
+ return isPolicyEnabled(policy);
+ }
+
+ bool isMMapSupported() {
+ return mMMapSupported;
+ }
+
+ bool isMMapExclusiveSupported() {
+ return mMMapExclusiveSupported;
+ }
+
+private:
+
+ enum {
+ AAUDIO_POLICY_NEVER = 1,
+ AAUDIO_POLICY_AUTO,
+ AAUDIO_POLICY_ALWAYS
+ };
+ typedef int32_t aaudio_policy_t;
+
+ int getIntegerProperty(const char *name, int defaultValue) {
+ int result = defaultValue;
+ char valueText[PROP_VALUE_MAX] = {0};
+ if (__system_property_get(name, valueText) != 0) {
+ result = atoi(valueText);
+ }
+ return result;
+ }
+
+ /**
+ * Load the function pointers.
+ * This can be called multiple times.
+ * It should only be called from one thread.
+ *
+ * @return 0 if successful or negative error.
+ */
+ aaudio_result_t loadSymbols() {
+ if (mAAudio_getMMapPolicy != nullptr) {
+ return 0;
+ }
+
+ void *libHandle = AAudioLoader::getInstance()->getLibHandle();
+ if (libHandle == nullptr) {
+ LOGI("%s() could not find " LIB_AAUDIO_NAME, __func__);
+ return AAUDIO_ERROR_UNAVAILABLE;
+ }
+
+ mAAudioStream_isMMap = (bool (*)(AAudioStream *stream))
+ dlsym(libHandle, FUNCTION_IS_MMAP);
+ if (mAAudioStream_isMMap == nullptr) {
+ LOGI("%s() could not find " FUNCTION_IS_MMAP, __func__);
+ return AAUDIO_ERROR_UNAVAILABLE;
+ }
+
+ mAAudio_setMMapPolicy = (int32_t (*)(aaudio_policy_t policy))
+ dlsym(libHandle, FUNCTION_SET_MMAP_POLICY);
+ if (mAAudio_setMMapPolicy == nullptr) {
+ LOGI("%s() could not find " FUNCTION_SET_MMAP_POLICY, __func__);
+ return AAUDIO_ERROR_UNAVAILABLE;
+ }
+
+ mAAudio_getMMapPolicy = (aaudio_policy_t (*)())
+ dlsym(libHandle, FUNCTION_GET_MMAP_POLICY);
+ if (mAAudio_getMMapPolicy == nullptr) {
+ LOGI("%s() could not find " FUNCTION_GET_MMAP_POLICY, __func__);
+ return AAUDIO_ERROR_UNAVAILABLE;
+ }
+
+ return 0;
+ }
+
+ bool mMMapSupported = false;
+ bool mMMapExclusiveSupported = false;
+
+ bool (*mAAudioStream_isMMap)(AAudioStream *stream) = nullptr;
+ int32_t (*mAAudio_setMMapPolicy)(aaudio_policy_t policy) = nullptr;
+ aaudio_policy_t (*mAAudio_getMMapPolicy)() = nullptr;
+};
+
+} // namespace oboe
+
+#endif //OBOE_AAUDIO_EXTENSIONS_H
diff --git a/src/aaudio/AAudioLoader.cpp b/src/aaudio/AAudioLoader.cpp
index 064633c6..ec97a26e 100644
--- a/src/aaudio/AAudioLoader.cpp
+++ b/src/aaudio/AAudioLoader.cpp
@@ -24,10 +24,17 @@
namespace oboe {
AAudioLoader::~AAudioLoader() {
- if (mLibHandle != nullptr) {
- dlclose(mLibHandle);
- mLibHandle = nullptr;
- }
+ // Issue 360: thread_local variables with non-trivial destructors
+ // will cause segfaults if the containing library is dlclose()ed on
+ // devices running M or newer, or devices before M when using a static STL.
+ // The simple workaround is to not call dlclose.
+ // https://github.com/android/ndk/wiki/Changelog-r22#known-issues
+ //
+ // The libaaudio and libaaudioclient do not use thread_local.
+ // But, to be safe, we should avoid dlclose() if possible.
+ // Because AAudioLoader is a static Singleton, we can safely skip
+ // calling dlclose() without causing a resource leak.
+ LOGI("%s() dlclose(%s) not called, OK", __func__, LIB_AAUDIO_NAME);
}
AAudioLoader* AAudioLoader::getInstance() {
@@ -90,8 +97,6 @@ int AAudioLoader::open() {
stream_getTimestamp = load_I_PSKPLPL("AAudioStream_getTimestamp");
- stream_isMMapUsed = load_B_PS("AAudioStream_isMMapUsed");
-
stream_getChannelCount = load_I_PS("AAudioStream_getChannelCount");
if (stream_getChannelCount == nullptr) {
// Use old alias if needed.
@@ -304,7 +309,6 @@ AAudioLoader::signature_I_PSKPLPL AAudioLoader::load_I_PSKPLPL(const char *funct
== AAUDIO_PERFORMANCE_MODE_POWER_SAVING, ERRMSG);
static_assert((int32_t)PerformanceMode::LowLatency
== AAUDIO_PERFORMANCE_MODE_LOW_LATENCY, ERRMSG);
-#endif
// The aaudio_ usage, content and input_preset types were added in NDK 17,
// which is the first version to support Android Pie (API 28).
@@ -343,6 +347,9 @@ AAudioLoader::signature_I_PSKPLPL AAudioLoader::load_I_PSKPLPL(const char *funct
static_assert((int32_t)SessionId::None == AAUDIO_SESSION_ID_NONE, ERRMSG);
static_assert((int32_t)SessionId::Allocate == AAUDIO_SESSION_ID_ALLOCATE, ERRMSG);
-#endif
+
+#endif // __NDK_MAJOR__ >= 17
+
+#endif // AAUDIO_AAUDIO_H
} // namespace oboe
diff --git a/src/aaudio/AAudioLoader.h b/src/aaudio/AAudioLoader.h
index 6f5bb950..a07c5391 100644
--- a/src/aaudio/AAudioLoader.h
+++ b/src/aaudio/AAudioLoader.h
@@ -52,6 +52,12 @@ typedef int32_t aaudio_usage_t;
typedef int32_t aaudio_content_type_t;
typedef int32_t aaudio_input_preset_t;
typedef int32_t aaudio_session_id_t;
+
+// There are a few definitions used by Oboe.
+#define AAUDIO_OK static_cast<aaudio_result_t>(Result::OK)
+#define AAUDIO_ERROR_TIMEOUT static_cast<aaudio_result_t>(Result::ErrorTimeout)
+#define AAUDIO_STREAM_STATE_STARTING static_cast<aaudio_stream_state_t>(StreamState::Starting)
+#define AAUDIO_STREAM_STATE_STARTED static_cast<aaudio_stream_state_t>(StreamState::Started)
#else
#include <aaudio/AAudio.h>
#include <android/ndk-version.h>
@@ -63,7 +69,6 @@ typedef int32_t aaudio_session_id_t;
namespace oboe {
-
/**
* The AAudio API was not available in early versions of Android.
* To avoid linker errors, we dynamically link with the functions by name using dlsym().
@@ -133,6 +138,8 @@ class AAudioLoader {
*/
int open();
+ void *getLibHandle() const { return mLibHandle; }
+
// Function pointers into the AAudio shared library.
signature_I_PPB createStreamBuilder = nullptr;
@@ -167,8 +174,6 @@ class AAudioLoader {
signature_I_PSKPLPL stream_getTimestamp = nullptr;
- signature_B_PS stream_isMMapUsed = nullptr;
-
signature_I_PS stream_close = nullptr;
signature_I_PS stream_getChannelCount = nullptr;
diff --git a/src/aaudio/AudioStreamAAudio.cpp b/src/aaudio/AudioStreamAAudio.cpp
index cb817623..bc535aeb 100644
--- a/src/aaudio/AudioStreamAAudio.cpp
+++ b/src/aaudio/AudioStreamAAudio.cpp
@@ -23,6 +23,7 @@
#include "common/AudioClock.h"
#include "common/OboeDebug.h"
#include "oboe/Utilities.h"
+#include "AAudioExtensions.h"
#ifdef __ANDROID__
#include <sys/system_properties.h>
@@ -677,7 +678,7 @@ ResultWithValue<double> AudioStreamAAudio::calculateLatencyMillis() {
bool AudioStreamAAudio::isMMapUsed() {
AAudioStream *stream = mAAudioStream.load();
if (stream != nullptr) {
- return mLibLoader->stream_isMMapUsed(stream);
+ return AAudioExtensions::getInstance().isMMapUsed(stream);
} else {
return false;
}
diff --git a/src/common/AudioStreamBuilder.cpp b/src/common/AudioStreamBuilder.cpp
index dffcd75e..b9f04b85 100644
--- a/src/common/AudioStreamBuilder.cpp
+++ b/src/common/AudioStreamBuilder.cpp
@@ -16,6 +16,8 @@
#include <sys/types.h>
+
+#include "aaudio/AAudioExtensions.h"
#include "aaudio/AudioStreamAAudio.h"
#include "FilterAudioStream.h"
#include "OboeDebug.h"
@@ -109,7 +111,6 @@ Result AudioStreamBuilder::openStream(AudioStream **streamPP) {
// Do we need to make a child stream and convert.
if (conversionNeeded) {
AudioStream *tempStream;
-
result = childBuilder.openStream(&tempStream);
if (result != Result::OK) {
return result;
@@ -156,7 +157,20 @@ Result AudioStreamBuilder::openStream(AudioStream **streamPP) {
}
}
- result = streamP->open(); // TODO review API
+ // If MMAP has a problem in this case then disable it temporarily.
+ bool wasMMapOriginallyEnabled = AAudioExtensions::getInstance().isMMapEnabled();
+ bool wasMMapTemporarilyDisabled = false;
+ if (wasMMapOriginallyEnabled) {
+ bool isMMapSafe = QuirksManager::getInstance().isMMapSafe(childBuilder);
+ if (!isMMapSafe) {
+ AAudioExtensions::getInstance().setMMapEnabled(false);
+ wasMMapTemporarilyDisabled = true;
+ }
+ }
+ result = streamP->open();
+ if (wasMMapTemporarilyDisabled) {
+ AAudioExtensions::getInstance().setMMapEnabled(wasMMapOriginallyEnabled); // restore original
+ }
if (result == Result::OK) {
int32_t optimalBufferSize = -1;
diff --git a/src/common/FilterAudioStream.h b/src/common/FilterAudioStream.h
index 210e9d11..5428db5b 100644
--- a/src/common/FilterAudioStream.h
+++ b/src/common/FilterAudioStream.h
@@ -173,7 +173,10 @@ public:
int64_t *timeNanoseconds) override {
int64_t childPosition = 0;
Result result = mChildStream->getTimestamp(clockId, &childPosition, timeNanoseconds);
- *framePosition = childPosition * mRateScaler;
+ // It is OK if framePosition is null.
+ if (framePosition) {
+ *framePosition = childPosition * mRateScaler;
+ }
return result;
}
diff --git a/src/common/QuirksManager.cpp b/src/common/QuirksManager.cpp
index aa285de0..5d35c71b 100644
--- a/src/common/QuirksManager.cpp
+++ b/src/common/QuirksManager.cpp
@@ -20,10 +20,6 @@
#include "OboeDebug.h"
#include "QuirksManager.h"
-#ifndef __ANDROID_API_R__
-#define __ANDROID_API_R__ 30
-#endif
-
using namespace oboe;
int32_t QuirksManager::DeviceQuirks::clipBufferSize(AudioStream &stream,
@@ -74,6 +70,9 @@ public:
std::string chipname = getPropertyString("ro.hardware.chipname");
isExynos9810 = (chipname == "exynos9810");
+ isExynos990 = (chipname == "exynos990");
+
+ mBuildChangelist = getPropertyInteger("ro.build.changelist", 0);
}
virtual ~SamsungDeviceQuirks() = default;
@@ -98,6 +97,17 @@ public:
&& builder.getInputPreset() != oboe::InputPreset::Camcorder;
}
+ bool isMMapSafe(const AudioStreamBuilder &builder) override {
+ const bool isInput = builder.getDirection() == Direction::Input;
+ // This detects b/159066712 , S20 LSI has corrupt low latency audio recording
+ // and turns off MMAP.
+ // See also https://github.com/google/oboe/issues/892
+ bool mRecordingCorrupted = isInput
+ && isExynos990
+ && mBuildChangelist < 19350896;
+ return !mRecordingCorrupted;
+ }
+
private:
// Stay farther away from DSP position on Exynos devices.
static constexpr int32_t kBottomMarginExynos = 2;
@@ -105,6 +115,8 @@ private:
static constexpr int32_t kTopMargin = 1;
bool isExynos = false;
bool isExynos9810 = false;
+ bool isExynos990 = false;
+ int mBuildChangelist = 0;
};
QuirksManager::QuirksManager() {
@@ -203,3 +215,8 @@ bool QuirksManager::isConversionNeeded(
return conversionNeeded;
}
+
+bool QuirksManager::isMMapSafe(AudioStreamBuilder &builder) {
+ if (!OboeGlobals::areWorkaroundsEnabled()) return true;
+ return mDeviceQuirks->isMMapSafe(builder);
+}
diff --git a/src/common/QuirksManager.h b/src/common/QuirksManager.h
index b4e38ded..aeca270a 100644
--- a/src/common/QuirksManager.h
+++ b/src/common/QuirksManager.h
@@ -21,6 +21,10 @@
#include <oboe/AudioStreamBuilder.h>
#include <aaudio/AudioStreamAAudio.h>
+#ifndef __ANDROID_API_R__
+#define __ANDROID_API_R__ 30
+#endif
+
namespace oboe {
/**
@@ -98,6 +102,10 @@ public:
virtual bool isAAudioMMapPossible(const AudioStreamBuilder &builder) const;
+ virtual bool isMMapSafe(const AudioStreamBuilder & /* builder */ ) {
+ return true;
+ }
+
static constexpr int32_t kDefaultBottomMarginInBursts = 0;
static constexpr int32_t kDefaultTopMarginInBursts = 0;
@@ -108,6 +116,8 @@ public:
static constexpr int32_t kCommonNativeRate = 48000; // very typical native sample rate
};
+ bool isMMapSafe(AudioStreamBuilder &builder);
+
private:
static constexpr int32_t kChannelCountMono = 1;
diff --git a/src/flowgraph/resampler/README.md b/src/flowgraph/resampler/README.md
index 5ea97dc8..eaea99f2 100644
--- a/src/flowgraph/resampler/README.md
+++ b/src/flowgraph/resampler/README.md
@@ -1,12 +1,20 @@
# Sample Rate Converter
This folder contains a sample rate converter, or "resampler".
-It is part of [Oboe](https://github.com/google/oboe) but has no dependencies on Oboe.
-So the contents of this folder can be used outside of Oboe.
The converter is based on a sinc function that has been windowed by a hyperbolic cosine.
We found this had fewer artifacts than the more traditional Kaiser window.
+## Building the Resampler
+
+It is part of [Oboe](https://github.com/google/oboe) but has no dependencies on Oboe.
+So the contents of this folder can be used outside of Oboe.
+
+To build it for use outside of Oboe:
+
+1. Copy the "resampler" folder to a folder in your project that is in the include path.
+2. Add all of the \*.cpp files in the resampler folder to your project IDE or Makefile.
+
## Creating a Resampler
Include the [main header](MultiChannelResampler.h) for the resampler.
diff --git a/tests/README.md b/tests/README.md
index 7ab84533..107e287d 100644
--- a/tests/README.md
+++ b/tests/README.md
@@ -31,9 +31,9 @@ Now we need to determine the latest installed version of the NDK. Enter:
ls $ANDROID_HOME/ndk
-Make note of the folder name. Mine was "21.0.6113669" so I entered:
+Make note of the folder name. Mine was "21.3.6528147" so I entered:
- export ANDROID_NDK=$ANDROID_HOME/ndk/21.0.6113669/
+ export ANDROID_NDK=$ANDROID_HOME/ndk/21.3.6528147/
If you need to add `cmake` to your path then you can find it by entering:
diff --git a/tests/testStreamOpen.cpp b/tests/testStreamOpen.cpp
index e2a91704..bdbfa505 100644
--- a/tests/testStreamOpen.cpp
+++ b/tests/testStreamOpen.cpp
@@ -70,6 +70,11 @@ protected:
usleep(50 * 1000);
timeout--;
}
+
+ // Catch Issue #1166
+ mStream->getTimestamp(CLOCK_MONOTONIC); // should not crash
+ mStream->getTimestamp(CLOCK_MONOTONIC, nullptr, nullptr); // should not crash
+
ASSERT_GT(callback.callbackCount, 0);
ASSERT_GT(callback.framesPerCallback, 0);
ASSERT_EQ(mStream->requestStop(), Result::OK);