aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRobert Wu <robertwu@google.com>2024-03-04 18:27:33 +0000
committerRobert Wu <robertwu@google.com>2024-03-04 18:33:02 +0000
commit8be3b19e265b2db28889b349179d2674988ab91f (patch)
treefe8db9c4ccb83f73b8fc3d3b58ca8faa8bac5f36
parentf2b11334a1e6e02f0c0240bd0db221b6f8483a31 (diff)
parenta0e3b6503742882ca0fffa68c29c2b3caf740276 (diff)
downloadoboe-8be3b19e265b2db28889b349179d2674988ab91f.tar.gz
Merge remote-tracking branch 'aosp/upstream-main' into external/oboe
Merging latest Oboe into external/oboe. Bug: 326989822 Test: mmm external/oboe Test: mmma cts/apps/CtsVerifier Test: mmm frameworks/av/media/libaaudio Change-Id: Id89715e13fb36e5bb116547b42fe725249f49e15
-rw-r--r--.github/workflows/build-ci.yml9
-rw-r--r--.github/workflows/update-docs.yml2
-rw-r--r--Android.bp3
-rw-r--r--CMakeLists.txt3
-rw-r--r--README.md2
-rw-r--r--apps/OboeTester/app/CMakeLists.txt2
-rw-r--r--apps/OboeTester/app/build.gradle17
-rw-r--r--apps/OboeTester/app/src/main/AndroidManifest.xml19
-rw-r--r--apps/OboeTester/app/src/main/cpp/AudioStreamGateway.cpp1
-rw-r--r--apps/OboeTester/app/src/main/cpp/FormatConverterBox.cpp12
-rw-r--r--apps/OboeTester/app/src/main/cpp/FormatConverterBox.h3
-rw-r--r--apps/OboeTester/app/src/main/cpp/FullDuplexAnalyzer.cpp8
-rw-r--r--apps/OboeTester/app/src/main/cpp/FullDuplexAnalyzer.h8
-rw-r--r--apps/OboeTester/app/src/main/cpp/FullDuplexEcho.cpp28
-rw-r--r--apps/OboeTester/app/src/main/cpp/FullDuplexEcho.h14
-rw-r--r--apps/OboeTester/app/src/main/cpp/FullDuplexStream.cpp152
-rw-r--r--apps/OboeTester/app/src/main/cpp/FullDuplexStream.h119
-rw-r--r--apps/OboeTester/app/src/main/cpp/FullDuplexStreamWithConversion.cpp60
-rw-r--r--apps/OboeTester/app/src/main/cpp/FullDuplexStreamWithConversion.h61
-rw-r--r--apps/OboeTester/app/src/main/cpp/InputStreamCallbackAnalyzer.cpp2
-rw-r--r--apps/OboeTester/app/src/main/cpp/InterpolatingDelayLine.cpp3
-rw-r--r--apps/OboeTester/app/src/main/cpp/InterpolatingDelayLine.h3
-rw-r--r--apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp83
-rw-r--r--apps/OboeTester/app/src/main/cpp/NativeAudioContext.h72
-rw-r--r--apps/OboeTester/app/src/main/cpp/OboeStreamCallbackProxy.cpp107
-rw-r--r--apps/OboeTester/app/src/main/cpp/OboeStreamCallbackProxy.h158
-rw-r--r--apps/OboeTester/app/src/main/cpp/OboeTesterStreamCallback.cpp52
-rw-r--r--apps/OboeTester/app/src/main/cpp/OboeTesterStreamCallback.h21
-rw-r--r--apps/OboeTester/app/src/main/cpp/OboeTools.h25
-rw-r--r--apps/OboeTester/app/src/main/cpp/TestColdStartLatency.cpp118
-rw-r--r--apps/OboeTester/app/src/main/cpp/TestColdStartLatency.h76
-rw-r--r--apps/OboeTester/app/src/main/cpp/TestRoutingCrash.cpp111
-rw-r--r--apps/OboeTester/app/src/main/cpp/TestRoutingCrash.h67
-rw-r--r--apps/OboeTester/app/src/main/cpp/analyzer/BaseSineAnalyzer.h14
-rw-r--r--apps/OboeTester/app/src/main/cpp/analyzer/DataPathAnalyzer.h15
-rw-r--r--apps/OboeTester/app/src/main/cpp/analyzer/GlitchAnalyzer.h143
-rw-r--r--apps/OboeTester/app/src/main/cpp/analyzer/InfiniteRecording.h5
-rw-r--r--apps/OboeTester/app/src/main/cpp/analyzer/LatencyAnalyzer.h105
-rw-r--r--apps/OboeTester/app/src/main/cpp/analyzer/ManchesterEncoder.h5
-rw-r--r--apps/OboeTester/app/src/main/cpp/jni-bridge.cpp235
-rw-r--r--apps/OboeTester/app/src/main/cpp/synth/BiquadFilter.h163
-rw-r--r--apps/OboeTester/app/src/main/cpp/synth/DifferentiatedParabola.h78
-rw-r--r--apps/OboeTester/app/src/main/cpp/synth/EnvelopeADSR.h229
-rw-r--r--apps/OboeTester/app/src/main/cpp/synth/IncludeMeOnce.h36
-rw-r--r--apps/OboeTester/app/src/main/cpp/synth/LookupTable.h72
-rw-r--r--apps/OboeTester/app/src/main/cpp/synth/PitchToFrequency.h104
-rw-r--r--apps/OboeTester/app/src/main/cpp/synth/SawtoothOscillator.h80
-rw-r--r--apps/OboeTester/app/src/main/cpp/synth/SawtoothOscillatorDPW.h53
-rw-r--r--apps/OboeTester/app/src/main/cpp/synth/SimpleVoice.h137
-rw-r--r--apps/OboeTester/app/src/main/cpp/synth/SineOscillator.h44
-rw-r--r--apps/OboeTester/app/src/main/cpp/synth/SquareOscillatorDPW.h73
-rw-r--r--apps/OboeTester/app/src/main/cpp/synth/SynthTools.h173
-rw-r--r--apps/OboeTester/app/src/main/cpp/synth/Synthesizer.h135
-rw-r--r--apps/OboeTester/app/src/main/cpp/synth/UnitGenerator.h55
-rw-r--r--apps/OboeTester/app/src/main/cpp/synth/VoiceBase.h58
-rw-r--r--apps/OboeTester/app/src/main/java/com/mobileer/audio_device/AudioDeviceAdapter.java4
-rw-r--r--apps/OboeTester/app/src/main/java/com/mobileer/audio_device/AudioDeviceInfoConverter.java57
-rw-r--r--apps/OboeTester/app/src/main/java/com/mobileer/audio_device/AudioDeviceListEntry.java14
-rw-r--r--apps/OboeTester/app/src/main/java/com/mobileer/audio_device/CommunicationDeviceSpinner.java21
-rw-r--r--apps/OboeTester/app/src/main/java/com/mobileer/miditools/synth/SynthEngine.java3
-rw-r--r--apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AnalyzerActivity.java30
-rw-r--r--apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioOutputTester.java4
-rw-r--r--apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioQueryTools.java24
-rw-r--r--apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioRecordThread.java7
-rw-r--r--apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioStreamBase.java86
-rw-r--r--apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioStreamTester.java4
-rw-r--r--apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AutomatedGlitchActivity.java39
-rw-r--r--apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AutomatedTestRunner.java20
-rw-r--r--apps/OboeTester/app/src/main/java/com/mobileer/oboetester/BaseAutoGlitchActivity.java210
-rw-r--r--apps/OboeTester/app/src/main/java/com/mobileer/oboetester/BaseOboeTesterActivity.java4
-rw-r--r--apps/OboeTester/app/src/main/java/com/mobileer/oboetester/BufferSizeView.java11
-rw-r--r--apps/OboeTester/app/src/main/java/com/mobileer/oboetester/CommunicationDeviceView.java111
-rw-r--r--apps/OboeTester/app/src/main/java/com/mobileer/oboetester/DeviceReportActivity.java126
-rw-r--r--apps/OboeTester/app/src/main/java/com/mobileer/oboetester/DynamicWorkloadActivity.java388
-rw-r--r--apps/OboeTester/app/src/main/java/com/mobileer/oboetester/EchoActivity.java30
-rw-r--r--apps/OboeTester/app/src/main/java/com/mobileer/oboetester/ExternalFileWriter.java55
-rw-r--r--apps/OboeTester/app/src/main/java/com/mobileer/oboetester/ExternalTapToToneActivity.java4
-rw-r--r--apps/OboeTester/app/src/main/java/com/mobileer/oboetester/ExtraTestsActivity.java11
-rw-r--r--apps/OboeTester/app/src/main/java/com/mobileer/oboetester/GlitchActivity.java87
-rw-r--r--apps/OboeTester/app/src/main/java/com/mobileer/oboetester/IntentBasedTestSupport.java29
-rw-r--r--apps/OboeTester/app/src/main/java/com/mobileer/oboetester/MainActivity.java42
-rw-r--r--apps/OboeTester/app/src/main/java/com/mobileer/oboetester/ManualGlitchActivity.java128
-rw-r--r--apps/OboeTester/app/src/main/java/com/mobileer/oboetester/MicrophoneInfoConverter.java6
-rw-r--r--apps/OboeTester/app/src/main/java/com/mobileer/oboetester/MultiLineChart.java254
-rw-r--r--apps/OboeTester/app/src/main/java/com/mobileer/oboetester/NativeEngine.java4
-rw-r--r--apps/OboeTester/app/src/main/java/com/mobileer/oboetester/NativeSniffer.java36
-rw-r--r--apps/OboeTester/app/src/main/java/com/mobileer/oboetester/OboeAudioOutputStream.java2
-rw-r--r--apps/OboeTester/app/src/main/java/com/mobileer/oboetester/OboeAudioStream.java74
-rw-r--r--apps/OboeTester/app/src/main/java/com/mobileer/oboetester/RoundTripLatencyActivity.java61
-rw-r--r--apps/OboeTester/app/src/main/java/com/mobileer/oboetester/StreamConfiguration.java157
-rw-r--r--apps/OboeTester/app/src/main/java/com/mobileer/oboetester/StreamConfigurationView.java45
-rw-r--r--apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TapToToneActivity.java56
-rw-r--r--apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TapToToneTester.java10
-rw-r--r--apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestAudioActivity.java241
-rw-r--r--apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestColdStartLatencyActivity.java210
-rw-r--r--apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestDataPathsActivity.java901
-rw-r--r--apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestDisconnectActivity.java209
-rw-r--r--apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestInputActivity.java29
-rw-r--r--apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestOutputActivity.java65
-rw-r--r--apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestOutputActivityBase.java9
-rw-r--r--apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestPlugLatencyActivity.java86
-rw-r--r--apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestRouteDuringCallbackActivity.java177
-rw-r--r--apps/OboeTester/app/src/main/java/com/mobileer/oboetester/VolumeBarView.java4
-rw-r--r--apps/OboeTester/app/src/main/java/com/mobileer/oboetester/WaveformView.java24
-rw-r--r--apps/OboeTester/app/src/main/java/com/mobileer/oboetester/WorkloadView.java43
-rw-r--r--apps/OboeTester/app/src/main/res/layout/activity_cold_start_latency.xml157
-rw-r--r--apps/OboeTester/app/src/main/res/layout/activity_data_paths.xml57
-rw-r--r--apps/OboeTester/app/src/main/res/layout/activity_dynamic_workload.xml120
-rw-r--r--apps/OboeTester/app/src/main/res/layout/activity_echo.xml25
-rw-r--r--apps/OboeTester/app/src/main/res/layout/activity_error_callback.xml4
-rw-r--r--apps/OboeTester/app/src/main/res/layout/activity_extra_tests.xml114
-rw-r--r--apps/OboeTester/app/src/main/res/layout/activity_main.xml35
-rw-r--r--apps/OboeTester/app/src/main/res/layout/activity_manual_glitches.xml27
-rw-r--r--apps/OboeTester/app/src/main/res/layout/activity_routing_crash.xml75
-rw-r--r--apps/OboeTester/app/src/main/res/layout/activity_rt_latency.xml8
-rw-r--r--apps/OboeTester/app/src/main/res/layout/activity_tap_to_tone.xml23
-rw-r--r--apps/OboeTester/app/src/main/res/layout/activity_test_disconnect.xml21
-rw-r--r--apps/OboeTester/app/src/main/res/layout/activity_test_input.xml24
-rw-r--r--apps/OboeTester/app/src/main/res/layout/activity_test_output.xml24
-rw-r--r--apps/OboeTester/app/src/main/res/layout/auto_test_runner.xml4
-rw-r--r--apps/OboeTester/app/src/main/res/layout/buffer_size_view.xml1
-rw-r--r--apps/OboeTester/app/src/main/res/layout/comm_device_view.xml22
-rw-r--r--apps/OboeTester/app/src/main/res/layout/merge_audio_common.xml76
-rw-r--r--apps/OboeTester/app/src/main/res/layout/sample_multi_line_chart.xml16
-rw-r--r--apps/OboeTester/app/src/main/res/layout/stream_config.xml22
-rw-r--r--apps/OboeTester/app/src/main/res/menu/menu_main.xml10
-rw-r--r--apps/OboeTester/app/src/main/res/values-night/styles.xml7
-rw-r--r--apps/OboeTester/app/src/main/res/values/attrs_fast_button.xml8
-rw-r--r--apps/OboeTester/app/src/main/res/values/attrs_multi_line_chart.xml10
-rw-r--r--apps/OboeTester/app/src/main/res/values/colors.xml5
-rw-r--r--apps/OboeTester/app/src/main/res/values/dimens.xml2
-rw-r--r--apps/OboeTester/app/src/main/res/values/strings.xml91
-rw-r--r--apps/OboeTester/app/src/main/res/values/styles.xml5
-rw-r--r--apps/OboeTester/docs/AutomatedTesting.md146
-rw-r--r--apps/OboeTester/docs/Usage.md70
-rw-r--r--apps/OboeTester/gradle.properties21
-rw-r--r--apps/fxlab/app/build.gradle4
-rw-r--r--apps/fxlab/app/src/main/kotlin/com/mobileer/androidfxlab/EffectsAdapter.kt (renamed from apps/fxlab/app/src/main/java/com/mobileer/androidfxlab/EffectsAdapter.kt)0
-rw-r--r--apps/fxlab/app/src/main/kotlin/com/mobileer/androidfxlab/MainActivity.kt (renamed from apps/fxlab/app/src/main/java/com/mobileer/androidfxlab/MainActivity.kt)0
-rw-r--r--apps/fxlab/app/src/main/kotlin/com/mobileer/androidfxlab/NativeInterface.kt (renamed from apps/fxlab/app/src/main/java/com/mobileer/androidfxlab/NativeInterface.kt)0
-rw-r--r--apps/fxlab/app/src/main/kotlin/com/mobileer/androidfxlab/datatype/Effect.kt (renamed from apps/fxlab/app/src/main/java/com/mobileer/androidfxlab/datatype/Effect.kt)0
-rw-r--r--apps/fxlab/app/src/main/kotlin/com/mobileer/androidfxlab/datatype/EffectDescription.kt (renamed from apps/fxlab/app/src/main/java/com/mobileer/androidfxlab/datatype/EffectDescription.kt)0
-rw-r--r--apps/fxlab/app/src/main/kotlin/com/mobileer/androidfxlab/datatype/ParamDescription.kt (renamed from apps/fxlab/app/src/main/java/com/mobileer/androidfxlab/datatype/ParamDescription.kt)0
-rwxr-xr-xbuild_all_android.sh2
-rw-r--r--docs/.nojekyll0
-rw-r--r--docs/AndroidAudioHistory.md4
-rw-r--r--docs/ChangeLog.md30
-rw-r--r--docs/OpenSLESMigration.md2
-rw-r--r--docs/README.md6
-rw-r--r--docs/notes/README.md11
-rw-r--r--docs/notes/disconnect.md76
-rw-r--r--docs/notes/effects.md48
-rw-r--r--docs/notes/rlsbuffer.md71
-rw-r--r--include/oboe/AudioStream.h91
-rw-r--r--include/oboe/AudioStreamBase.h69
-rw-r--r--include/oboe/AudioStreamBuilder.h86
-rw-r--r--include/oboe/AudioStreamCallback.h3
-rw-r--r--include/oboe/Definitions.h188
-rw-r--r--include/oboe/FullDuplexStream.h324
-rw-r--r--include/oboe/Oboe.h1
-rw-r--r--include/oboe/Utilities.h10
-rw-r--r--include/oboe/Version.h4
-rw-r--r--samples/LiveEffect/build.gradle4
-rw-r--r--samples/LiveEffect/src/main/cpp/CMakeLists.txt1
-rw-r--r--samples/LiveEffect/src/main/cpp/FullDuplexPass.h8
-rw-r--r--samples/LiveEffect/src/main/cpp/FullDuplexStream.cpp128
-rw-r--r--samples/LiveEffect/src/main/cpp/FullDuplexStream.h102
-rw-r--r--samples/LiveEffect/src/main/cpp/LiveEffectEngine.cpp20
-rw-r--r--samples/MegaDrone/README.md2
-rw-r--r--samples/MegaDrone/build.gradle4
-rw-r--r--samples/MegaDrone/src/main/java/com/google/oboe/samples/megadrone/MainActivity.java2
-rw-r--r--samples/README.md5
-rw-r--r--samples/RhythmGame/build.gradle4
-rw-r--r--samples/RhythmGame/src/main/java/com/google/oboe/samples/rhythmgame/MainActivity.java2
-rwxr-xr-xsamples/RhythmGame/test/run_tests.sh2
-rw-r--r--samples/SoundBoard/README.md1
-rw-r--r--samples/SoundBoard/build.gradle9
-rw-r--r--samples/SoundBoard/src/main/AndroidManifest.xml1
-rw-r--r--samples/SoundBoard/src/main/cpp/Synth.h4
-rw-r--r--samples/SoundBoard/src/main/java/com/google/oboe/samples/soundboard/MainActivity.java111
-rw-r--r--samples/SoundBoard/src/main/java/com/google/oboe/samples/soundboard/MusicTileView.java169
-rw-r--r--samples/SoundBoard/src/main/kotlin/com/google/oboe/samples/soundboard/MainActivity.kt132
-rw-r--r--samples/SoundBoard/src/main/kotlin/com/google/oboe/samples/soundboard/MusicTileView.kt164
-rw-r--r--samples/SoundBoard/src/main/kotlin/com/google/oboe/samples/soundboard/NoteListener.kt (renamed from samples/SoundBoard/src/main/java/com/google/oboe/samples/soundboard/NoteListener.java)23
-rw-r--r--samples/audio-device/build.gradle4
-rw-r--r--samples/drumthumper/README.md9
-rw-r--r--samples/drumthumper/build.gradle4
-rw-r--r--samples/drumthumper/drumthumper-screenshot.pngbin0 -> 118282 bytes
-rw-r--r--samples/drumthumper/src/main/cpp/DrumPlayerJNI.cpp10
-rw-r--r--samples/drumthumper/src/main/kotlin/com/plausibleaudio/drumthumper/DrumPlayer.kt (renamed from samples/drumthumper/src/main/java/com/plausibleaudio/drumthumper/DrumPlayer.kt)38
-rw-r--r--samples/drumthumper/src/main/kotlin/com/plausibleaudio/drumthumper/DrumThumperActivity.kt (renamed from samples/drumthumper/src/main/java/com/plausibleaudio/drumthumper/DrumThumperActivity.kt)11
-rw-r--r--samples/drumthumper/src/main/kotlin/com/plausibleaudio/drumthumper/TriggerPad.kt (renamed from samples/drumthumper/src/main/java/com/plausibleaudio/drumthumper/TriggerPad.kt)0
-rw-r--r--samples/hello-oboe/build.gradle4
-rw-r--r--samples/iolib/build.gradle4
-rw-r--r--samples/iolib/src/main/cpp/player/OneShotSampleSource.cpp39
-rw-r--r--samples/iolib/src/main/cpp/player/SampleBuffer.cpp27
-rw-r--r--samples/iolib/src/main/cpp/player/SampleBuffer.h2
-rw-r--r--samples/iolib/src/main/cpp/player/SampleSource.h8
-rw-r--r--samples/iolib/src/main/cpp/player/SimpleMultiPlayer.cpp68
-rw-r--r--samples/iolib/src/main/cpp/player/SimpleMultiPlayer.h47
-rw-r--r--samples/minimaloboe/README.md3
-rw-r--r--samples/minimaloboe/minimaloboe-screenshot.pngbin0 -> 98980 bytes
-rw-r--r--samples/minimaloboe/src/main/kotlin/com/example/minimaloboe/AudioPlayer.kt (renamed from samples/minimaloboe/src/main/java/com/example/minimaloboe/AudioPlayer.kt)2
-rw-r--r--samples/minimaloboe/src/main/kotlin/com/example/minimaloboe/MainActivity.kt (renamed from samples/minimaloboe/src/main/java/com/example/minimaloboe/MainActivity.kt)0
-rw-r--r--samples/minimaloboe/src/main/kotlin/com/example/minimaloboe/ui/theme/Color.kt (renamed from samples/minimaloboe/src/main/java/com/example/minimaloboe/ui/theme/Color.kt)0
-rw-r--r--samples/minimaloboe/src/main/kotlin/com/example/minimaloboe/ui/theme/Shape.kt (renamed from samples/minimaloboe/src/main/java/com/example/minimaloboe/ui/theme/Shape.kt)0
-rw-r--r--samples/minimaloboe/src/main/kotlin/com/example/minimaloboe/ui/theme/Theme.kt (renamed from samples/minimaloboe/src/main/java/com/example/minimaloboe/ui/theme/Theme.kt)0
-rw-r--r--samples/minimaloboe/src/main/kotlin/com/example/minimaloboe/ui/theme/Type.kt (renamed from samples/minimaloboe/src/main/java/com/example/minimaloboe/ui/theme/Type.kt)0
-rw-r--r--samples/parselib/build.gradle4
-rw-r--r--samples/shared/Mixer.h32
-rw-r--r--samples/shared/SynthSound.h6
-rw-r--r--src/aaudio/AAudioExtensions.h5
-rw-r--r--src/aaudio/AAudioLoader.cpp62
-rw-r--r--src/aaudio/AAudioLoader.h40
-rw-r--r--src/aaudio/AudioStreamAAudio.cpp153
-rw-r--r--src/aaudio/AudioStreamAAudio.h20
-rw-r--r--src/common/AdpfWrapper.cpp124
-rw-r--r--src/common/AdpfWrapper.h85
-rw-r--r--src/common/AudioStream.cpp6
-rw-r--r--src/common/AudioStreamBuilder.cpp8
-rw-r--r--src/common/FilterAudioStream.h4
-rw-r--r--src/common/QuirksManager.cpp7
-rw-r--r--src/common/Utilities.cpp16
-rw-r--r--src/flowgraph/FlowgraphUtilities.h15
-rw-r--r--src/flowgraph/SinkI8_24.cpp55
-rw-r--r--src/flowgraph/SinkI8_24.h40
-rw-r--r--src/flowgraph/SourceI8_24.cpp54
-rw-r--r--src/flowgraph/SourceI8_24.h42
-rw-r--r--src/flowgraph/resampler/MultiChannelResampler.cpp9
-rw-r--r--src/flowgraph/resampler/MultiChannelResampler.h7
-rw-r--r--src/flowgraph/resampler/PolyphaseResampler.cpp1
-rw-r--r--src/flowgraph/resampler/SincResampler.cpp1
-rw-r--r--src/flowgraph/resampler/SincResamplerStereo.cpp1
-rw-r--r--src/opensles/AudioInputStreamOpenSLES.cpp2
-rw-r--r--src/opensles/AudioOutputStreamOpenSLES.cpp13
-rw-r--r--src/opensles/AudioStreamOpenSLES.cpp26
-rw-r--r--src/opensles/OpenSLESUtilities.cpp1
-rw-r--r--tests/CMakeLists.txt1
-rw-r--r--tests/README1
-rw-r--r--tests/UnitTestRunner/app/build.gradle4
-rw-r--r--tests/UnitTestRunner/app/src/main/java/com/google/oboe/tests/unittestrunner/MainActivity.java1
-rw-r--r--tests/UnitTestRunner/app/src/main/res/values/strings.xml2
-rwxr-xr-xtests/run_tests.sh2
-rw-r--r--tests/testFullDuplexStream.cpp208
-rw-r--r--tests/testResampler.cpp24
-rw-r--r--tests/testStreamClosedMethods.cpp53
-rw-r--r--tests/testStreamOpen.cpp285
247 files changed, 9976 insertions, 2567 deletions
diff --git a/.github/workflows/build-ci.yml b/.github/workflows/build-ci.yml
index f3f5f28b..42d40a2f 100644
--- a/.github/workflows/build-ci.yml
+++ b/.github/workflows/build-ci.yml
@@ -11,11 +11,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
- - name: set up JDK 11
- uses: actions/setup-java@v1
+ - uses: actions/checkout@v3
+ - name: set up JDK 17
+ uses: actions/setup-java@v3
with:
- java-version: 11
+ distribution: 'temurin'
+ java-version: 17
- name: build samples and apps
uses: github/codeql-action/init@v2
with:
diff --git a/.github/workflows/update-docs.yml b/.github/workflows/update-docs.yml
index a37d05e9..a563e7f0 100644
--- a/.github/workflows/update-docs.yml
+++ b/.github/workflows/update-docs.yml
@@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
- name: Doxygen Action
uses: mattnotmitt/doxygen-action@v1.1.0
diff --git a/Android.bp b/Android.bp
index 2116bcae..c3d2e744 100644
--- a/Android.bp
+++ b/Android.bp
@@ -54,6 +54,7 @@ cc_library_static {
name: "oboe",
srcs: [
"src/aaudio/AAudioLoader.cpp",
+ "src/common/AdpfWrapper.cpp",
"src/aaudio/AudioStreamAAudio.cpp",
"src/common/AudioSourceCaller.cpp",
"src/common/AudioStream.cpp",
@@ -90,10 +91,12 @@ cc_library_static {
"src/flowgraph/SinkI16.cpp",
"src/flowgraph/SinkI24.cpp",
"src/flowgraph/SinkI32.cpp",
+ "src/flowgraph/SinkI8_24.cpp",
"src/flowgraph/SourceFloat.cpp",
"src/flowgraph/SourceI16.cpp",
"src/flowgraph/SourceI24.cpp",
"src/flowgraph/SourceI32.cpp",
+ "src/flowgraph/SourceI8_24.cpp",
"src/flowgraph/resampler/IntegerRatio.cpp",
"src/flowgraph/resampler/LinearResampler.cpp",
"src/flowgraph/resampler/MultiChannelResampler.cpp",
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 056011c5..ac798aac 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -9,6 +9,7 @@ project(oboe)
set (oboe_sources
src/aaudio/AAudioLoader.cpp
src/aaudio/AudioStreamAAudio.cpp
+ src/common/AdpfWrapper.cpp
src/common/AudioSourceCaller.cpp
src/common/AudioStream.cpp
src/common/AudioStreamBuilder.cpp
@@ -44,10 +45,12 @@ set (oboe_sources
src/flowgraph/SinkI16.cpp
src/flowgraph/SinkI24.cpp
src/flowgraph/SinkI32.cpp
+ src/flowgraph/SinkI8_24.cpp
src/flowgraph/SourceFloat.cpp
src/flowgraph/SourceI16.cpp
src/flowgraph/SourceI24.cpp
src/flowgraph/SourceI32.cpp
+ src/flowgraph/SourceI8_24.cpp
src/flowgraph/resampler/IntegerRatio.cpp
src/flowgraph/resampler/LinearResampler.cpp
src/flowgraph/resampler/MultiChannelResampler.cpp
diff --git a/README.md b/README.md
index 44acf9f6..37713380 100644
--- a/README.md
+++ b/README.md
@@ -16,10 +16,10 @@ Oboe is a C++ library which makes it easy to build high-performance audio apps o
- [Getting Started Guide](docs/GettingStarted.md)
- [Full Guide to Oboe](docs/FullGuide.md)
- [API reference](https://google.github.io/oboe)
-- [Tech Notes](docs/notes/)
- [History of Audio features/bugs by Android version](docs/AndroidAudioHistory.md)
- [Migration guide for apps using OpenSL ES](docs/OpenSLESMigration.md)
- [Frequently Asked Questions](docs/FAQ.md) (FAQ)
+- [Wiki](https://github.com/google/oboe/wiki)
- [Our roadmap](https://github.com/google/oboe/milestones) - Vote on a feature/issue by adding a thumbs up to the first comment.
### Community
diff --git a/apps/OboeTester/app/CMakeLists.txt b/apps/OboeTester/app/CMakeLists.txt
index 8409dcab..8361d882 100644
--- a/apps/OboeTester/app/CMakeLists.txt
+++ b/apps/OboeTester/app/CMakeLists.txt
@@ -6,7 +6,7 @@ set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3")
link_directories(${CMAKE_CURRENT_LIST_DIR}/..)
-# Increment this number when adding files to OboeTester => 101
+# Increment this number when adding files to OboeTester => 104
# The change in this file will help Android Studio resync
# and generate new build files that reference the new code.
file(GLOB_RECURSE app_native_sources src/main/cpp/*)
diff --git a/apps/OboeTester/app/build.gradle b/apps/OboeTester/app/build.gradle
index 91c62a80..0c093f70 100644
--- a/apps/OboeTester/app/build.gradle
+++ b/apps/OboeTester/app/build.gradle
@@ -1,13 +1,13 @@
apply plugin: 'com.android.application'
android {
- compileSdkVersion 33
+ compileSdkVersion 34
defaultConfig {
applicationId = "com.mobileer.oboetester"
minSdkVersion 23
- targetSdkVersion 33
- versionCode 65
- versionName "2.3.6"
+ targetSdkVersion 34
+ versionCode 82
+ versionName "2.5.11"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
externalNativeBuild {
cmake {
@@ -34,10 +34,9 @@ android {
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
- implementation 'com.android.support.constraint:constraint-layout:2.0.4'
+ implementation "androidx.core:core-ktx:1.9.0"
+ implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
- testImplementation 'junit:junit:4.13-beta-3'
- implementation 'com.android.support:appcompat-v7:28.0.0'
- androidTestImplementation 'com.android.support.test:runner:1.0.2'
- androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.5'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
}
diff --git a/apps/OboeTester/app/src/main/AndroidManifest.xml b/apps/OboeTester/app/src/main/AndroidManifest.xml
index 98628186..fdfd8d95 100644
--- a/apps/OboeTester/app/src/main/AndroidManifest.xml
+++ b/apps/OboeTester/app/src/main/AndroidManifest.xml
@@ -20,10 +20,7 @@
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.INTERNET" />
- <!-- This is needed for sharing test results. -->
- <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
- <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<application
android:allowBackup="false"
@@ -108,6 +105,20 @@
android:name=".TestErrorCallbackActivity"
android:label="@string/title_error_callback"
android:exported="true" />
+ <activity
+ android:name=".TestRouteDuringCallbackActivity"
+ android:label="@string/title_route_during_callback"
+ android:exported="true" />
+
+ <activity
+ android:name=".DynamicWorkloadActivity"
+ android:label="@string/title_dynamic_load"
+ android:exported="true" />
+
+ <activity
+ android:name=".TestColdStartLatencyActivity"
+ android:label="@string/title_cold_start_latency"
+ android:exported="true" />
<service
android:name=".MidiTapTester"
@@ -123,7 +134,7 @@
</service>
<provider
- android:name="android.support.v4.content.FileProvider"
+ android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
diff --git a/apps/OboeTester/app/src/main/cpp/AudioStreamGateway.cpp b/apps/OboeTester/app/src/main/cpp/AudioStreamGateway.cpp
index c4735405..6867d140 100644
--- a/apps/OboeTester/app/src/main/cpp/AudioStreamGateway.cpp
+++ b/apps/OboeTester/app/src/main/cpp/AudioStreamGateway.cpp
@@ -28,6 +28,7 @@ oboe::DataCallbackResult AudioStreamGateway::onAudioReady(
void *audioData,
int numFrames) {
+ maybeHang(getNanoseconds());
printScheduler();
if (mAudioSink != nullptr) {
diff --git a/apps/OboeTester/app/src/main/cpp/FormatConverterBox.cpp b/apps/OboeTester/app/src/main/cpp/FormatConverterBox.cpp
index dd9f324f..0974b4c4 100644
--- a/apps/OboeTester/app/src/main/cpp/FormatConverterBox.cpp
+++ b/apps/OboeTester/app/src/main/cpp/FormatConverterBox.cpp
@@ -16,18 +16,20 @@
#include "FormatConverterBox.h"
-FormatConverterBox::FormatConverterBox(int32_t numSamples,
+FormatConverterBox::FormatConverterBox(int32_t maxSamples,
oboe::AudioFormat inputFormat,
oboe::AudioFormat outputFormat) {
mInputFormat = inputFormat;
mOutputFormat = outputFormat;
- mInputBuffer = std::make_unique<uint8_t[]>(numSamples * sizeof(int32_t));
- mOutputBuffer = std::make_unique<uint8_t[]>(numSamples * sizeof(int32_t));
+ mMaxSamples = maxSamples;
+ mInputBuffer = std::make_unique<uint8_t[]>(maxSamples * sizeof(int32_t));
+ mOutputBuffer = std::make_unique<uint8_t[]>(maxSamples * sizeof(int32_t));
mSource.reset();
switch (mInputFormat) {
case oboe::AudioFormat::I16:
+ case oboe::AudioFormat::IEC61937:
mSource = std::make_unique<oboe::flowgraph::SourceI16>(1);
break;
case oboe::AudioFormat::I24:
@@ -46,6 +48,7 @@ FormatConverterBox::FormatConverterBox(int32_t numSamples,
mSink.reset();
switch (mOutputFormat) {
case oboe::AudioFormat::I16:
+ case oboe::AudioFormat::IEC61937:
mSink = std::make_unique<oboe::flowgraph::SinkI16>(1);
break;
case oboe::AudioFormat::I24:
@@ -68,14 +71,17 @@ FormatConverterBox::FormatConverterBox(int32_t numSamples,
}
int32_t FormatConverterBox::convertInternalBuffers(int32_t numSamples) {
+ assert(numSamples <= mMaxSamples);
return convert(getOutputBuffer(), numSamples, getInputBuffer());
}
int32_t FormatConverterBox::convertToInternalOutput(int32_t numSamples, const void *inputBuffer) {
+ assert(numSamples <= mMaxSamples);
return convert(getOutputBuffer(), numSamples, inputBuffer);
}
int32_t FormatConverterBox::convertFromInternalInput(void *outputBuffer, int32_t numSamples) {
+ assert(numSamples <= mMaxSamples);
return convert(outputBuffer, numSamples, getInputBuffer());
}
diff --git a/apps/OboeTester/app/src/main/cpp/FormatConverterBox.h b/apps/OboeTester/app/src/main/cpp/FormatConverterBox.h
index 782f9e0c..4a673b3a 100644
--- a/apps/OboeTester/app/src/main/cpp/FormatConverterBox.h
+++ b/apps/OboeTester/app/src/main/cpp/FormatConverterBox.h
@@ -38,7 +38,7 @@
class FormatConverterBox {
public:
- FormatConverterBox(int32_t numSamples,
+ FormatConverterBox(int32_t maxSamples,
oboe::AudioFormat inputFormat,
oboe::AudioFormat outputFormat);
@@ -90,6 +90,7 @@ private:
oboe::AudioFormat mInputFormat{oboe::AudioFormat::Invalid};
oboe::AudioFormat mOutputFormat{oboe::AudioFormat::Invalid};
+ int32_t mMaxSamples = 0;
std::unique_ptr<uint8_t[]> mInputBuffer;
std::unique_ptr<uint8_t[]> mOutputBuffer;
diff --git a/apps/OboeTester/app/src/main/cpp/FullDuplexAnalyzer.cpp b/apps/OboeTester/app/src/main/cpp/FullDuplexAnalyzer.cpp
index 58252f5a..243c647c 100644
--- a/apps/OboeTester/app/src/main/cpp/FullDuplexAnalyzer.cpp
+++ b/apps/OboeTester/app/src/main/cpp/FullDuplexAnalyzer.cpp
@@ -21,10 +21,10 @@ oboe::Result FullDuplexAnalyzer::start() {
getLoopbackProcessor()->setSampleRate(getOutputStream()->getSampleRate());
getLoopbackProcessor()->prepareToTest();
mWriteReadDeltaValid = false;
- return FullDuplexStream::start();
+ return FullDuplexStreamWithConversion::start();
}
-oboe::DataCallbackResult FullDuplexAnalyzer::onBothStreamsReady(
+oboe::DataCallbackResult FullDuplexAnalyzer::onBothStreamsReadyFloat(
const float *inputData,
int numInputFrames,
float *outputData,
@@ -32,7 +32,7 @@ oboe::DataCallbackResult FullDuplexAnalyzer::onBothStreamsReady(
int32_t inputStride = getInputStream()->getChannelCount();
int32_t outputStride = getOutputStream()->getChannelCount();
- const float *inputFloat = inputData;
+ auto *inputFloat = static_cast<const float *>(inputData);
float *outputFloat = outputData;
// Get atomic snapshot of the relative frame positions so they
@@ -56,7 +56,7 @@ oboe::DataCallbackResult FullDuplexAnalyzer::onBothStreamsReady(
inputFloat += inputStride;
mRecording->write(buffer, 1);
}
- // Handle mismatch in in numFrames.
+ // Handle mismatch in numFrames.
buffer[0] = 0.0f; // gap in output
for (int i = numBoth; i < numInputFrames; i++) {
buffer[1] = *inputFloat;
diff --git a/apps/OboeTester/app/src/main/cpp/FullDuplexAnalyzer.h b/apps/OboeTester/app/src/main/cpp/FullDuplexAnalyzer.h
index 0473d3d4..4163aa82 100644
--- a/apps/OboeTester/app/src/main/cpp/FullDuplexAnalyzer.h
+++ b/apps/OboeTester/app/src/main/cpp/FullDuplexAnalyzer.h
@@ -21,22 +21,22 @@
#include <sys/types.h>
#include "oboe/Oboe.h"
-#include "FullDuplexStream.h"
#include "analyzer/LatencyAnalyzer.h"
+#include "FullDuplexStreamWithConversion.h"
#include "MultiChannelRecording.h"
-class FullDuplexAnalyzer : public FullDuplexStream {
+class FullDuplexAnalyzer : public FullDuplexStreamWithConversion {
public:
FullDuplexAnalyzer(LoopbackProcessor *processor)
: mLoopbackProcessor(processor) {
- setMNumInputBurstsCushion(1);
+ setNumInputBurstsCushion(1);
}
/**
* Called when data is available on both streams.
* Caller should override this method.
*/
- oboe::DataCallbackResult onBothStreamsReady(
+ oboe::DataCallbackResult onBothStreamsReadyFloat(
const float *inputData,
int numInputFrames,
float *outputData,
diff --git a/apps/OboeTester/app/src/main/cpp/FullDuplexEcho.cpp b/apps/OboeTester/app/src/main/cpp/FullDuplexEcho.cpp
index 91530241..d54f669d 100644
--- a/apps/OboeTester/app/src/main/cpp/FullDuplexEcho.cpp
+++ b/apps/OboeTester/app/src/main/cpp/FullDuplexEcho.cpp
@@ -20,17 +20,31 @@
oboe::Result FullDuplexEcho::start() {
int32_t delayFrames = (int32_t) (kMaxDelayTimeSeconds * getOutputStream()->getSampleRate());
mDelayLine = std::make_unique<InterpolatingDelayLine>(delayFrames);
- return FullDuplexStream::start();
+ // Use peak detector for input streams
+ mNumChannels = getInputStream()->getChannelCount();
+ mPeakDetectors = std::make_unique<PeakDetector[]>(mNumChannels);
+ return FullDuplexStreamWithConversion::start();
}
-oboe::DataCallbackResult FullDuplexEcho::onBothStreamsReady(
+double FullDuplexEcho::getPeakLevel(int index) {
+ if (mPeakDetectors == nullptr) {
+ LOGE("%s() called before setup()", __func__);
+ return -1.0;
+ } else if (index < 0 || index >= mNumChannels) {
+ LOGE("%s(), index out of range, 0 <= %d < %d", __func__, index, mNumChannels.load());
+ return -2.0;
+ }
+ return mPeakDetectors[index].getLevel();
+}
+
+oboe::DataCallbackResult FullDuplexEcho::onBothStreamsReadyFloat(
const float *inputData,
int numInputFrames,
float *outputData,
int numOutputFrames) {
int32_t framesToEcho = std::min(numInputFrames, numOutputFrames);
- float *inputFloat = (float *)inputData;
- float *outputFloat = (float *)outputData;
+ auto *inputFloat = const_cast<float *>(inputData);
+ float *outputFloat = outputData;
// zero out entire output array
memset(outputFloat, 0, static_cast<size_t>(numOutputFrames)
* static_cast<size_t>(getOutputStream()->getBytesPerFrame()));
@@ -40,6 +54,12 @@ oboe::DataCallbackResult FullDuplexEcho::onBothStreamsReady(
float delayFrames = mDelayTimeSeconds * getOutputStream()->getSampleRate();
while (framesToEcho-- > 0) {
*outputFloat = mDelayLine->process(delayFrames, *inputFloat); // mono delay
+
+ for (int iChannel = 0; iChannel < inputStride; iChannel++) {
+ float sample = * (inputFloat + iChannel);
+ mPeakDetectors[iChannel].process(sample);
+ }
+
inputFloat += inputStride;
outputFloat += outputStride;
}
diff --git a/apps/OboeTester/app/src/main/cpp/FullDuplexEcho.h b/apps/OboeTester/app/src/main/cpp/FullDuplexEcho.h
index cbb69d7e..a88e51ef 100644
--- a/apps/OboeTester/app/src/main/cpp/FullDuplexEcho.h
+++ b/apps/OboeTester/app/src/main/cpp/FullDuplexEcho.h
@@ -21,20 +21,21 @@
#include <sys/types.h>
#include "oboe/Oboe.h"
-#include "FullDuplexStream.h"
+#include "analyzer/LatencyAnalyzer.h"
+#include "FullDuplexStreamWithConversion.h"
#include "InterpolatingDelayLine.h"
-class FullDuplexEcho : public FullDuplexStream {
+class FullDuplexEcho : public FullDuplexStreamWithConversion {
public:
FullDuplexEcho() {
- setMNumInputBurstsCushion(0);
+ setNumInputBurstsCushion(0);
}
/**
* Called when data is available on both streams.
* Caller should override this method.
*/
- oboe::DataCallbackResult onBothStreamsReady(
+ oboe::DataCallbackResult onBothStreamsReadyFloat(
const float *inputData,
int numInputFrames,
float *outputData,
@@ -43,6 +44,8 @@ public:
oboe::Result start() override;
+ double getPeakLevel(int index);
+
void setDelayTime(double delayTimeSeconds) {
mDelayTimeSeconds = delayTimeSeconds;
}
@@ -51,6 +54,9 @@ private:
std::unique_ptr<InterpolatingDelayLine> mDelayLine;
static constexpr double kMaxDelayTimeSeconds = 4.0;
double mDelayTimeSeconds = kMaxDelayTimeSeconds;
+ std::atomic<int32_t> mNumChannels{0};
+ std::unique_ptr<PeakDetector[]> mPeakDetectors;
+
};
diff --git a/apps/OboeTester/app/src/main/cpp/FullDuplexStream.cpp b/apps/OboeTester/app/src/main/cpp/FullDuplexStream.cpp
deleted file mode 100644
index 52e762e6..00000000
--- a/apps/OboeTester/app/src/main/cpp/FullDuplexStream.cpp
+++ /dev/null
@@ -1,152 +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 "FullDuplexStream.h"
-
-oboe::ResultWithValue<int32_t> FullDuplexStream::readInput(int32_t numFrames) {
- oboe::ResultWithValue<int32_t> result = getInputStream()->read(
- mInputConverter->getInputBuffer(),
- numFrames,
- 0 /* timeout */);
- if (result == oboe::Result::OK) {
- int32_t numSamples = result.value() * getInputStream()->getChannelCount();
- mInputConverter->convertInternalBuffers(numSamples);
- }
- return result;
-}
-
-oboe::DataCallbackResult FullDuplexStream::onAudioReady(
- oboe::AudioStream *outputStream,
- void *audioData,
- int numFrames) {
- oboe::DataCallbackResult callbackResult = oboe::DataCallbackResult::Continue;
- int32_t actualFramesRead = 0;
-
- // Silence the output.
- int32_t numBytes = numFrames * outputStream->getBytesPerFrame();
- memset(audioData, 0 /* value */, numBytes);
-
- if (mCountCallbacksToDrain > 0) {
- // Drain the input.
- int32_t totalFramesRead = 0;
- do {
- oboe::ResultWithValue<int32_t> result = readInput(numFrames);
- if (!result) {
- // Ignore errors because input stream may not be started yet.
- break;
- }
- actualFramesRead = result.value();
- totalFramesRead += actualFramesRead;
- } while (actualFramesRead > 0);
- // Only counts if we actually got some data.
- if (totalFramesRead > 0) {
- mCountCallbacksToDrain--;
- }
-
- } else if (mCountInputBurstsCushion > 0) {
- // Let the input fill up a bit so we are not so close to the write pointer.
- mCountInputBurstsCushion--;
-
- } else if (mCountCallbacksToDiscard > 0) {
- mCountCallbacksToDiscard--;
- // Ignore. Allow the input to reach to equilibrium with the output.
- oboe::ResultWithValue<int32_t> resultAvailable = getInputStream()->getAvailableFrames();
- if (!resultAvailable) {
- LOGE("%s() getAvailableFrames() returned %s\n",
- __func__, convertToText(resultAvailable.error()));
- callbackResult = oboe::DataCallbackResult::Stop;
- } else {
- int32_t framesAvailable = resultAvailable.value();
- if (framesAvailable >= mMinimumFramesBeforeRead) {
- oboe::ResultWithValue<int32_t> resultRead = readInput(numFrames);
- if (!resultRead) {
- LOGE("%s() read() returned %s\n", __func__, convertToText(resultRead.error()));
- callbackResult = oboe::DataCallbackResult::Stop;
- }
- }
- }
- } else {
- int32_t framesRead = 0;
- oboe::ResultWithValue<int32_t> resultAvailable = getInputStream()->getAvailableFrames();
- if (!resultAvailable) {
- LOGE("%s() getAvailableFrames() returned %s\n", __func__, convertToText(resultAvailable.error()));
- callbackResult = oboe::DataCallbackResult::Stop;
- } else {
- int32_t framesAvailable = resultAvailable.value();
- if (framesAvailable >= mMinimumFramesBeforeRead) {
- // Read data into input buffer.
- oboe::ResultWithValue<int32_t> resultRead = readInput(numFrames);
- if (!resultRead) {
- LOGE("%s() read() returned %s\n", __func__, convertToText(resultRead.error()));
- callbackResult = oboe::DataCallbackResult::Stop;
- } else {
- framesRead = resultRead.value();
- }
- }
- }
-
- if (callbackResult == oboe::DataCallbackResult::Continue) {
- callbackResult = onBothStreamsReady(
- (const float *) mInputConverter->getOutputBuffer(),
- framesRead,
- (float *) mOutputConverter->getInputBuffer(), numFrames);
- mOutputConverter->convertFromInternalInput( audioData,
- numFrames * getOutputStream()->getChannelCount());
- }
- }
-
- if (callbackResult == oboe::DataCallbackResult::Stop) {
- getInputStream()->requestStop();
- }
-
- return callbackResult;
-}
-
-oboe::Result FullDuplexStream::start() {
- mCountCallbacksToDrain = kNumCallbacksToDrain;
- mCountInputBurstsCushion = mNumInputBurstsCushion;
- mCountCallbacksToDiscard = kNumCallbacksToDiscard;
-
- // Determine maximum size that could possibly be called.
- int32_t bufferSize = getOutputStream()->getBufferCapacityInFrames()
- * getOutputStream()->getChannelCount();
- mInputConverter = std::make_unique<FormatConverterBox>(bufferSize,
- getInputStream()->getFormat(),
- oboe::AudioFormat::Float);
- mOutputConverter = std::make_unique<FormatConverterBox>(bufferSize,
- oboe::AudioFormat::Float,
- getOutputStream()->getFormat());
-
- oboe::Result result = getInputStream()->requestStart();
- if (result != oboe::Result::OK) {
- return result;
- }
- return getOutputStream()->requestStart();
-}
-
-oboe::Result FullDuplexStream::stop() {
- getOutputStream()->requestStop(); // TODO result?
- return getInputStream()->requestStop();
-}
-
-int32_t FullDuplexStream::getMNumInputBurstsCushion() const {
- return mNumInputBurstsCushion;
-}
-
-void FullDuplexStream::setMNumInputBurstsCushion(int32_t numBursts) {
- FullDuplexStream::mNumInputBurstsCushion = numBursts;
-}
diff --git a/apps/OboeTester/app/src/main/cpp/FullDuplexStream.h b/apps/OboeTester/app/src/main/cpp/FullDuplexStream.h
deleted file mode 100644
index dcfb6ea4..00000000
--- a/apps/OboeTester/app/src/main/cpp/FullDuplexStream.h
+++ /dev/null
@@ -1,119 +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_STREAM_H
-#define OBOETESTER_FULL_DUPLEX_STREAM_H
-
-#include <unistd.h>
-#include <sys/types.h>
-
-#include "oboe/Oboe.h"
-
-#include "FormatConverterBox.h"
-
-class FullDuplexStream : public oboe::AudioStreamCallback {
-public:
- FullDuplexStream() {}
- virtual ~FullDuplexStream() = default;
-
- void setInputStream(oboe::AudioStream *stream) {
- mInputStream = stream;
- }
-
- oboe::AudioStream *getInputStream() {
- return mInputStream;
- }
-
- void setOutputStream(oboe::AudioStream *stream) {
- mOutputStream = stream;
- }
- oboe::AudioStream *getOutputStream() {
- return mOutputStream;
- }
-
- virtual oboe::Result start();
-
- virtual oboe::Result stop();
-
- oboe::ResultWithValue<int32_t> readInput(int32_t numFrames);
-
- /**
- * Called when data is available on both streams.
- * Caller should override this method.
- */
- virtual oboe::DataCallbackResult onBothStreamsReady(
- const float *inputData,
- int numInputFrames,
- float *outputData,
- int numOutputFrames
- ) = 0;
-
- /**
- * Called by Oboe when the stream is ready to process audio.
- */
- oboe::DataCallbackResult onAudioReady(
- oboe::AudioStream *audioStream,
- void *audioData,
- int numFrames) override;
-
- int32_t getMNumInputBurstsCushion() const;
-
- /**
- * Number of bursts to leave in the input buffer as a cushion.
- * Typically 0 for latency measurements
- * or 1 for glitch tests.
- *
- * @param mNumInputBurstsCushion
- */
- void setMNumInputBurstsCushion(int32_t mNumInputBurstsCushion);
-
- void setMinimumFramesBeforeRead(int32_t numFrames) {
- mMinimumFramesBeforeRead = numFrames;
- }
-
- int32_t getMinimumFramesBeforeRead() const {
- return mMinimumFramesBeforeRead;
- }
-
-private:
-
- // TODO add getters and setters
- static constexpr int32_t kNumCallbacksToDrain = 20;
- static constexpr int32_t kNumCallbacksToDiscard = 30;
-
- // let input fill back up, usually 0 or 1
- int32_t mNumInputBurstsCushion = 0;
- int32_t mMinimumFramesBeforeRead = 0;
-
- // We want to reach a state where the input buffer is empty and
- // the output buffer is full.
- // These are used in order.
- // Drain several callback so that input is empty.
- int32_t mCountCallbacksToDrain = kNumCallbacksToDrain;
- // Let the input fill back up slightly so we don't run dry.
- int32_t mCountInputBurstsCushion = mNumInputBurstsCushion;
- // Discard some callbacks so the input and output reach equilibrium.
- int32_t mCountCallbacksToDiscard = kNumCallbacksToDiscard;
-
- oboe::AudioStream *mInputStream = nullptr;
- oboe::AudioStream *mOutputStream = nullptr;
-
- std::unique_ptr<FormatConverterBox> mInputConverter;
- std::unique_ptr<FormatConverterBox> mOutputConverter;
-};
-
-
-#endif //OBOETESTER_FULL_DUPLEX_STREAM_H
diff --git a/apps/OboeTester/app/src/main/cpp/FullDuplexStreamWithConversion.cpp b/apps/OboeTester/app/src/main/cpp/FullDuplexStreamWithConversion.cpp
new file mode 100644
index 00000000..a64b8d96
--- /dev/null
+++ b/apps/OboeTester/app/src/main/cpp/FullDuplexStreamWithConversion.cpp
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "common/OboeDebug.h"
+#include "FullDuplexStreamWithConversion.h"
+
+oboe::Result FullDuplexStreamWithConversion::start() {
+ // Determine maximum size that could possibly be called.
+ int32_t bufferSize = getOutputStream()->getBufferCapacityInFrames()
+ * getOutputStream()->getChannelCount();
+ mInputConverter = std::make_unique<FormatConverterBox>(bufferSize,
+ getInputStream()->getFormat(),
+ oboe::AudioFormat::Float);
+ mOutputConverter = std::make_unique<FormatConverterBox>(bufferSize,
+ oboe::AudioFormat::Float,
+ getOutputStream()->getFormat());
+ return FullDuplexStream::start();
+}
+
+oboe::ResultWithValue<int32_t> FullDuplexStreamWithConversion::readInput(int32_t numFrames) {
+ oboe::ResultWithValue<int32_t> result = getInputStream()->read(
+ mInputConverter->getInputBuffer(),
+ numFrames,
+ 0 /* timeout */);
+ if (result == oboe::Result::OK) {
+ int32_t numSamples = result.value() * getInputStream()->getChannelCount();
+ mInputConverter->convertInternalBuffers(numSamples);
+ }
+ return result;
+}
+
+oboe::DataCallbackResult FullDuplexStreamWithConversion::onBothStreamsReady(
+ const void *inputData,
+ int numInputFrames,
+ void *outputData,
+ int numOutputFrames
+) {
+ oboe::DataCallbackResult callbackResult = oboe::DataCallbackResult::Continue;
+ callbackResult = onBothStreamsReadyFloat(
+ static_cast<const float *>(mInputConverter->getOutputBuffer()),
+ numInputFrames,
+ static_cast<float *>(mOutputConverter->getInputBuffer()),
+ numOutputFrames);
+ mOutputConverter->convertFromInternalInput( outputData,
+ numOutputFrames * getOutputStream()->getChannelCount());
+ return callbackResult;
+}
diff --git a/apps/OboeTester/app/src/main/cpp/FullDuplexStreamWithConversion.h b/apps/OboeTester/app/src/main/cpp/FullDuplexStreamWithConversion.h
new file mode 100644
index 00000000..63c2242e
--- /dev/null
+++ b/apps/OboeTester/app/src/main/cpp/FullDuplexStreamWithConversion.h
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef OBOETESTER_FULL_DUPLEX_STREAM_WITH_CONVERSION_H
+#define OBOETESTER_FULL_DUPLEX_STREAM_WITH_CONVERSION_H
+
+#include <unistd.h>
+#include <sys/types.h>
+
+#include "oboe/Oboe.h"
+#include "FormatConverterBox.h"
+
+class FullDuplexStreamWithConversion : public oboe::FullDuplexStream {
+public:
+ /**
+ * Called when data is available on both streams.
+ * Caller must override this method.
+ */
+ virtual oboe::DataCallbackResult onBothStreamsReadyFloat(
+ const float *inputData,
+ int numInputFrames,
+ float *outputData,
+ int numOutputFrames
+ ) = 0;
+
+ /**
+ * Overrides the default onBothStreamsReady by converting to floats and then calling
+ * onBothStreamsReadyFloat().
+ */
+ oboe::DataCallbackResult onBothStreamsReady(
+ const void *inputData,
+ int numInputFrames,
+ void *outputData,
+ int numOutputFrames
+ ) override;
+
+ oboe::ResultWithValue<int32_t> readInput(int32_t numFrames) override;
+
+ virtual oboe::Result start() override;
+
+private:
+ std::unique_ptr<FormatConverterBox> mInputConverter;
+ std::unique_ptr<FormatConverterBox> mOutputConverter;
+
+};
+
+
+#endif //OBOETESTER_FULL_DUPLEX_STREAM_WITH_CONVERSION_H
diff --git a/apps/OboeTester/app/src/main/cpp/InputStreamCallbackAnalyzer.cpp b/apps/OboeTester/app/src/main/cpp/InputStreamCallbackAnalyzer.cpp
index 6ac37599..73c1bbed 100644
--- a/apps/OboeTester/app/src/main/cpp/InputStreamCallbackAnalyzer.cpp
+++ b/apps/OboeTester/app/src/main/cpp/InputStreamCallbackAnalyzer.cpp
@@ -33,6 +33,8 @@ oboe::DataCallbackResult InputStreamCallbackAnalyzer::onAudioReady(
void *audioData,
int numFrames) {
int32_t channelCount = audioStream->getChannelCount();
+
+ maybeHang(getNanoseconds());
printScheduler();
mInputConverter->convertToInternalOutput(numFrames * channelCount, audioData);
float *floatData = (float *) mInputConverter->getOutputBuffer();
diff --git a/apps/OboeTester/app/src/main/cpp/InterpolatingDelayLine.cpp b/apps/OboeTester/app/src/main/cpp/InterpolatingDelayLine.cpp
index e69c7112..f5393c57 100644
--- a/apps/OboeTester/app/src/main/cpp/InterpolatingDelayLine.cpp
+++ b/apps/OboeTester/app/src/main/cpp/InterpolatingDelayLine.cpp
@@ -14,7 +14,8 @@
* limitations under the License.
*/
-#include "common/OboeDebug.h"
+#include <algorithm>
+
#include "InterpolatingDelayLine.h"
InterpolatingDelayLine::InterpolatingDelayLine(int32_t delaySize) {
diff --git a/apps/OboeTester/app/src/main/cpp/InterpolatingDelayLine.h b/apps/OboeTester/app/src/main/cpp/InterpolatingDelayLine.h
index b3a510da..b05e23fb 100644
--- a/apps/OboeTester/app/src/main/cpp/InterpolatingDelayLine.h
+++ b/apps/OboeTester/app/src/main/cpp/InterpolatingDelayLine.h
@@ -21,9 +21,6 @@
#include <unistd.h>
#include <sys/types.h>
-#include "oboe/Oboe.h"
-#include "FullDuplexStream.h"
-
/**
* Monophonic delay line.
*/
diff --git a/apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp b/apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp
index 5e327fd3..2aa6eeb9 100644
--- a/apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp
+++ b/apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp
@@ -99,6 +99,16 @@ int32_t ActivityContext::allocateStreamIndex() {
return mNextStreamHandle++;
}
+oboe::Result ActivityContext::release() {
+ oboe::Result result = oboe::Result::OK;
+ stopBlockingIOThread();
+ for (auto entry : mOboeStreams) {
+ std::shared_ptr<oboe::AudioStream> oboeStream = entry.second;
+ result = oboeStream->release();
+ }
+ return result;
+}
+
void ActivityContext::close(int32_t streamIndex) {
stopBlockingIOThread();
std::shared_ptr<oboe::AudioStream> oboeStream = getStream(streamIndex);
@@ -153,6 +163,7 @@ int ActivityContext::open(jint nativeApi,
jint inputPreset,
jint usage,
jint contentType,
+ jint bufferCapacityInFrames,
jint deviceId,
jint sessionId,
jboolean channelConversionAllowed,
@@ -192,6 +203,7 @@ int ActivityContext::open(jint nativeApi,
->setInputPreset((oboe::InputPreset)inputPreset)
->setUsage((oboe::Usage)usage)
->setContentType((oboe::ContentType)contentType)
+ ->setBufferCapacityInFrames(bufferCapacityInFrames)
->setDeviceId(deviceId)
->setSessionId((oboe::SessionId) sessionId)
->setSampleRate(sampleRate)
@@ -245,7 +257,12 @@ int ActivityContext::open(jint nativeApi,
dataBuffer = std::make_unique<float[]>(numSamples);
}
- return (result != Result::OK) ? (int)result : streamIndex;
+ if (result != Result::OK) {
+ return (int) result;
+ } else {
+ configureAfterOpen();
+ return streamIndex;
+ }
}
oboe::Result ActivityContext::start() {
@@ -257,8 +274,6 @@ oboe::Result ActivityContext::start() {
return oboe::Result::ErrorInvalidState; // not open
}
- configureForStart();
-
audioStreamGateway.reset();
result = startStreams();
@@ -285,6 +300,15 @@ oboe::Result ActivityContext::start() {
return result;
}
+oboe::Result ActivityContext::flush() {
+ oboe::Result result = oboe::Result::OK;
+ for (auto entry : mOboeStreams) {
+ std::shared_ptr<oboe::AudioStream> oboeStream = entry.second;
+ result = oboeStream->requestFlush();
+ }
+ return result;
+}
+
int32_t ActivityContext::saveWaveFile(const char *filename) {
if (mRecording == nullptr) {
LOGW("ActivityContext::saveWaveFile(%s) but no recording!", filename);
@@ -334,6 +358,7 @@ void ActivityTestOutput::close(int32_t streamIndex) {
ActivityContext::close(streamIndex);
manyToMulti.reset(nullptr);
monoToMulti.reset(nullptr);
+ mVolumeRamp.reset();
mSinkFloat.reset();
mSinkI16.reset();
mSinkI24.reset();
@@ -372,16 +397,21 @@ void ActivityTestOutput::setChannelEnabled(int channelIndex, bool enabled) {
}
}
-void ActivityTestOutput::configureForStart() {
+void ActivityTestOutput::configureAfterOpen() {
manyToMulti = std::make_unique<ManyToMultiConverter>(mChannelCount);
+ std::shared_ptr<oboe::AudioStream> outputStream = getOutputStream();
+
+ mVolumeRamp = std::make_shared<RampLinear>(mChannelCount);
+ mVolumeRamp->setLengthInFrames(kRampMSec * outputStream->getSampleRate() /
+ MILLISECONDS_PER_SECOND);
+ mVolumeRamp->setTarget(mAmplitude);
+
mSinkFloat = std::make_shared<SinkFloat>(mChannelCount);
mSinkI16 = std::make_shared<SinkI16>(mChannelCount);
mSinkI24 = std::make_shared<SinkI24>(mChannelCount);
mSinkI32 = std::make_shared<SinkI32>(mChannelCount);
- std::shared_ptr<oboe::AudioStream> outputStream = getOutputStream();
-
mTriangleOscillator.setSampleRate(outputStream->getSampleRate());
mTriangleOscillator.frequency.setValue(1.0/kSweepPeriod);
mTriangleOscillator.amplitude.setValue(1.0);
@@ -412,10 +442,12 @@ void ActivityTestOutput::configureForStart() {
mWhiteNoise.amplitude.setValue(0.5);
- manyToMulti->output.connect(&(mSinkFloat.get()->input));
- manyToMulti->output.connect(&(mSinkI16.get()->input));
- manyToMulti->output.connect(&(mSinkI24.get()->input));
- manyToMulti->output.connect(&(mSinkI32.get()->input));
+ manyToMulti->output.connect(&(mVolumeRamp.get()->input));
+
+ mVolumeRamp->output.connect(&(mSinkFloat.get()->input));
+ mVolumeRamp->output.connect(&(mSinkI16.get()->input));
+ mVolumeRamp->output.connect(&(mSinkI24.get()->input));
+ mVolumeRamp->output.connect(&(mSinkI32.get()->input));
mSinkFloat->pullReset();
mSinkI16->pullReset();
@@ -438,7 +470,7 @@ void ActivityTestOutput::configureStreamGateway() {
}
if (mUseCallback) {
- oboeCallbackProxy.setCallback(&audioStreamGateway);
+ oboeCallbackProxy.setDataCallback(&audioStreamGateway);
}
}
@@ -475,11 +507,22 @@ void ActivityTestOutput::runBlockingIO() {
}
}
+oboe::Result ActivityTestOutput::startStreams() {
+ mSinkFloat->pullReset();
+ mSinkI16->pullReset();
+ mSinkI24->pullReset();
+ mSinkI32->pullReset();
+ if (mVolumeRamp != nullptr) {
+ mVolumeRamp->setTarget(mAmplitude);
+ }
+ return getOutputStream()->start();
+}
+
// ======================================================================= ActivityTestInput
-void ActivityTestInput::configureForStart() {
+void ActivityTestInput::configureAfterOpen() {
mInputAnalyzer.reset();
if (mUseCallback) {
- oboeCallbackProxy.setCallback(&mInputAnalyzer);
+ oboeCallbackProxy.setDataCallback(&mInputAnalyzer);
}
mInputAnalyzer.setRecording(mRecording.get());
}
@@ -556,7 +599,7 @@ oboe::Result ActivityRecording::startPlayback() {
}
// ======================================================================= ActivityTapToTone
-void ActivityTapToTone::configureForStart() {
+void ActivityTapToTone::configureAfterOpen() {
monoToMulti = std::make_unique<MonoToMultiConverter>(mChannelCount);
mSinkFloat = std::make_shared<SinkFloat>(mChannelCount);
@@ -606,7 +649,7 @@ void ActivityEcho::configureBuilder(bool isInput, oboe::AudioStreamBuilder &buil
// only output uses a callback, input is polled
if (!isInput) {
builder.setCallback((oboe::AudioStreamCallback *) &oboeCallbackProxy);
- oboeCallbackProxy.setCallback(mFullDuplexEcho.get());
+ oboeCallbackProxy.setDataCallback(mFullDuplexEcho.get());
}
}
@@ -628,7 +671,7 @@ void ActivityRoundTripLatency::configureBuilder(bool isInput, oboe::AudioStreamB
if (!isInput) {
// only output uses a callback, input is polled
builder.setCallback((oboe::AudioStreamCallback *) &oboeCallbackProxy);
- oboeCallbackProxy.setCallback(mFullDuplexLatency.get());
+ oboeCallbackProxy.setDataCallback(mFullDuplexLatency.get());
}
}
@@ -683,7 +726,7 @@ void ActivityGlitches::configureBuilder(bool isInput, oboe::AudioStreamBuilder &
if (!isInput) {
// only output uses a callback, input is polled
builder.setCallback((oboe::AudioStreamCallback *) &oboeCallbackProxy);
- oboeCallbackProxy.setCallback(mFullDuplexGlitches.get());
+ oboeCallbackProxy.setDataCallback(mFullDuplexGlitches.get());
}
}
@@ -706,7 +749,7 @@ void ActivityDataPath::configureBuilder(bool isInput, oboe::AudioStreamBuilder &
if (!isInput) {
// only output uses a callback, input is polled
builder.setCallback((oboe::AudioStreamCallback *) &oboeCallbackProxy);
- oboeCallbackProxy.setCallback(mFullDuplexDataPath.get());
+ oboeCallbackProxy.setDataCallback(mFullDuplexDataPath.get());
}
}
@@ -725,7 +768,7 @@ void ActivityTestDisconnect::close(int32_t streamIndex) {
mSinkFloat.reset();
}
-void ActivityTestDisconnect::configureForStart() {
+void ActivityTestDisconnect::configureAfterOpen() {
std::shared_ptr<oboe::AudioStream> outputStream = getOutputStream();
std::shared_ptr<oboe::AudioStream> inputStream = getInputStream();
if (outputStream) {
@@ -744,6 +787,6 @@ void ActivityTestDisconnect::configureForStart() {
} else if (inputStream) {
audioStreamGateway.setAudioSink(nullptr);
}
- oboeCallbackProxy.setCallback(&audioStreamGateway);
+ oboeCallbackProxy.setDataCallback(&audioStreamGateway);
}
diff --git a/apps/OboeTester/app/src/main/cpp/NativeAudioContext.h b/apps/OboeTester/app/src/main/cpp/NativeAudioContext.h
index 9ffd36e4..94ae680d 100644
--- a/apps/OboeTester/app/src/main/cpp/NativeAudioContext.h
+++ b/apps/OboeTester/app/src/main/cpp/NativeAudioContext.h
@@ -32,6 +32,7 @@
#include "flowunits/ImpulseOscillator.h"
#include "flowgraph/ManyToMultiConverter.h"
#include "flowgraph/MonoToMultiConverter.h"
+#include "flowgraph/RampLinear.h"
#include "flowgraph/SinkFloat.h"
#include "flowgraph/SinkI16.h"
#include "flowgraph/SinkI24.h"
@@ -45,12 +46,12 @@
#include "FullDuplexAnalyzer.h"
#include "FullDuplexEcho.h"
-#include "FullDuplexStream.h"
#include "analyzer/GlitchAnalyzer.h"
#include "analyzer/DataPathAnalyzer.h"
#include "InputStreamCallbackAnalyzer.h"
#include "MultiChannelRecording.h"
#include "OboeStreamCallbackProxy.h"
+#include "OboeTools.h"
#include "PlayRecordingCallback.h"
#include "SawPingGenerator.h"
@@ -66,9 +67,6 @@
#define AMPLITUDE_SAW_PING 0.8
#define AMPLITUDE_IMPULSE 0.7
-#define NANOS_PER_MICROSECOND ((int64_t) 1000)
-#define NANOS_PER_MILLISECOND (1000 * NANOS_PER_MICROSECOND)
-#define NANOS_PER_SECOND (1000 * NANOS_PER_MILLISECOND)
#define SECONDS_TO_RECORD 10
@@ -123,6 +121,7 @@ public:
jint inputPreset,
jint usage,
jint contentType,
+ jint bufferCapacityInFrames,
jint deviceId,
jint sessionId,
jboolean channelConversionAllowed,
@@ -131,32 +130,48 @@ public:
jboolean isMMap,
jboolean isInput);
+ oboe::Result release();
+
virtual void close(int32_t streamIndex);
- virtual void configureForStart() {}
+ virtual void configureAfterOpen() {}
oboe::Result start();
oboe::Result pause();
+ oboe::Result flush();
+
oboe::Result stopAllStreams();
virtual oboe::Result stop() {
return stopAllStreams();
}
- double getCpuLoad() {
+ float getCpuLoad() {
return oboeCallbackProxy.getCpuLoad();
}
+ float getAndResetMaxCpuLoad() {
+ return oboeCallbackProxy.getAndResetMaxCpuLoad();
+ }
+
+ uint32_t getAndResetCpuMask() {
+ return oboeCallbackProxy.getAndResetCpuMask();
+ }
+
std::string getCallbackTimeString() {
return oboeCallbackProxy.getCallbackTimeString();
}
- void setWorkload(double workload) {
+ void setWorkload(int32_t workload) {
oboeCallbackProxy.setWorkload(workload);
}
+ void setHearWorkload(bool enabled) {
+ oboeCallbackProxy.setHearWorkload(enabled);
+ }
+
virtual oboe::Result startPlayback() {
return oboe::Result::OK;
}
@@ -270,6 +285,8 @@ public:
virtual void setSignalType(int signalType) {}
+ virtual void setAmplitude(float amplitude) {}
+
virtual int32_t saveWaveFile(const char *filename);
virtual void setMinimumFramesBeforeRead(int32_t numFrames) {}
@@ -279,6 +296,10 @@ public:
double getTimestampLatency(int32_t streamIndex);
+ void setCpuAffinityMask(uint32_t mask) {
+ oboeCallbackProxy.setCpuAffinityMask(mask);
+ }
+
protected:
std::shared_ptr<oboe::AudioStream> getInputStream();
std::shared_ptr<oboe::AudioStream> getOutputStream();
@@ -324,7 +345,7 @@ public:
ActivityTestInput() {}
virtual ~ActivityTestInput() = default;
- void configureForStart() override;
+ void configureAfterOpen() override;
double getPeakLevel(int index) override {
return mInputAnalyzer.getPeakLevel(index);
@@ -345,7 +366,7 @@ protected:
oboe::Result startStreams() override {
mInputAnalyzer.reset();
- mInputAnalyzer.setup(getInputStream()->getFramesPerBurst(),
+ mInputAnalyzer.setup(std::max(getInputStream()->getFramesPerBurst(), callbackSize),
getInputStream()->getChannelCount(),
getInputStream()->getFormat());
return getInputStream()->requestStart();
@@ -394,11 +415,9 @@ public:
void close(int32_t streamIndex) override;
- oboe::Result startStreams() override {
- return getOutputStream()->start();
- }
+ oboe::Result startStreams() override;
- void configureForStart() override;
+ void configureAfterOpen() override;
virtual void configureStreamGateway();
@@ -419,6 +438,13 @@ public:
mSignalType = (SignalType) signalType;
}
+ void setAmplitude(float amplitude) override {
+ mAmplitude = amplitude;
+ if (mVolumeRamp) {
+ mVolumeRamp->setTarget(mAmplitude);
+ }
+ }
+
protected:
SignalType mSignalType = SignalType::Sine;
@@ -432,6 +458,10 @@ protected:
ExponentialShape mExponentialShape;
class WhiteNoise mWhiteNoise;
+ static constexpr int kRampMSec = 10; // for volume control
+ float mAmplitude = 1.0f;
+ std::shared_ptr<RampLinear> mVolumeRamp;
+
std::unique_ptr<ManyToMultiConverter> manyToMulti;
std::unique_ptr<MonoToMultiConverter> monoToMulti;
std::shared_ptr<oboe::flowgraph::SinkFloat> mSinkFloat;
@@ -449,7 +479,7 @@ public:
ActivityTapToTone() {}
virtual ~ActivityTapToTone() = default;
- void configureForStart() override;
+ void configureAfterOpen() override;
virtual void trigger() override {
sawPingGenerator.trigger();
@@ -505,6 +535,10 @@ public:
}
}
+ double getPeakLevel(int index) override {
+ return mFullDuplexEcho->getPeakLevel(index);
+ }
+
FullDuplexAnalyzer *getFullDuplexAnalyzer() override {
return (FullDuplexAnalyzer *) mFullDuplexEcho.get();
}
@@ -643,7 +677,8 @@ public:
void configureBuilder(bool isInput, oboe::AudioStreamBuilder &builder) override;
- void configureForStart() override {
+ void configureAfterOpen() override {
+ // set buffer size
std::shared_ptr<oboe::AudioStream> outputStream = getOutputStream();
int32_t capacityInFrames = outputStream->getBufferCapacityInFrames();
int32_t burstInFrames = outputStream->getFramesPerBurst();
@@ -697,7 +732,7 @@ public:
return oboe::Result::ErrorNull;
}
- void configureForStart() override;
+ void configureAfterOpen() override;
private:
std::unique_ptr<SineOscillator> sineOscillator;
@@ -706,7 +741,8 @@ private:
};
/**
- * Switch between various
+ * Global context for native tests.
+ * Switch between various ActivityContexts.
*/
class NativeAudioContext {
public:
@@ -764,7 +800,6 @@ public:
ActivityDataPath mActivityDataPath;
ActivityTestDisconnect mActivityTestDisconnect;
-
private:
// WARNING - must match definitions in TestAudioActivity.java
@@ -783,7 +818,6 @@ private:
ActivityType mActivityType = ActivityType::Undefined;
ActivityContext *currentActivity = &mActivityTestOutput;
-
};
#endif //NATIVEOBOE_NATIVEAUDIOCONTEXT_H
diff --git a/apps/OboeTester/app/src/main/cpp/OboeStreamCallbackProxy.cpp b/apps/OboeTester/app/src/main/cpp/OboeStreamCallbackProxy.cpp
index 987a1d31..20a20e05 100644
--- a/apps/OboeTester/app/src/main/cpp/OboeStreamCallbackProxy.cpp
+++ b/apps/OboeTester/app/src/main/cpp/OboeStreamCallbackProxy.cpp
@@ -17,40 +17,8 @@
#include "common/OboeDebug.h"
#include "OboeStreamCallbackProxy.h"
-// Linear congruential random number generator.
-static uint32_t s_random16() {
- static uint32_t seed = 1234;
- seed = ((seed * 31421) + 6927) & 0x0FFFF;
- return seed;
-}
-
-/**
- * The random number generator is good for burning CPU because the compiler cannot
- * easily optimize away the computation.
- * @param workload number of times to execute the loop
- * @return a white noise value between -1.0 and +1.0
- */
-static float s_burnCPU(int32_t workload) {
- uint32_t random = 0;
- for (int32_t i = 0; i < workload; i++) {
- for (int32_t j = 0; j < 10; j++) {
- random = random ^ s_random16();
- }
- }
- return (random - 32768) * (1.0 / 32768);
-}
-
bool OboeStreamCallbackProxy::mCallbackReturnStop = false;
-int64_t OboeStreamCallbackProxy::getNanoseconds(clockid_t clockId) {
- struct timespec time;
- int result = clock_gettime(clockId, &time);
- if (result < 0) {
- return result;
- }
- return (time.tv_sec * 1e9) + time.tv_nsec;
-}
-
oboe::DataCallbackResult OboeStreamCallbackProxy::onAudioReady(
oboe::AudioStream *audioStream,
void *audioData,
@@ -58,6 +26,18 @@ oboe::DataCallbackResult OboeStreamCallbackProxy::onAudioReady(
oboe::DataCallbackResult callbackResult = oboe::DataCallbackResult::Stop;
int64_t startTimeNanos = getNanoseconds();
+ // Record which CPU this is running on.
+ orCurrentCpuMask(sched_getcpu());
+
+ // Change affinity if app requested a change.
+ uint32_t mask = mCpuAffinityMask;
+ if (mask != mPreviousMask) {
+ int err = applyCpuAffinityMask(mask);
+ if (err != 0) {
+ }
+ mPreviousMask = mask;
+ }
+
mCallbackCount++;
mFramesPerCallback = numFrames;
@@ -65,24 +45,67 @@ oboe::DataCallbackResult OboeStreamCallbackProxy::onAudioReady(
return oboe::DataCallbackResult::Stop;
}
- s_burnCPU((int32_t)(mWorkload * kWorkloadScaler * numFrames));
-
if (mCallback != nullptr) {
callbackResult = mCallback->onAudioReady(audioStream, audioData, numFrames);
}
- // Update CPU load
- double calculationTime = (double)(getNanoseconds() - startTimeNanos);
- double inverseRealTime = audioStream->getSampleRate() / (1.0e9 * numFrames);
- double currentCpuLoad = calculationTime * inverseRealTime; // avoid a divide
- mCpuLoad = (mCpuLoad * 0.95) + (currentCpuLoad * 0.05); // simple low pass filter
+ mSynthWorkload.onCallback(mNumWorkloadVoices);
+ if (mNumWorkloadVoices > 0) {
+ // Render into the buffer or discard the synth voices.
+ float *buffer = (audioStream->getChannelCount() == 2 && mHearWorkload)
+ ? static_cast<float *>(audioData) : nullptr;
+ mSynthWorkload.renderStereo(buffer, numFrames);
+ }
- int64_t currentTimeNs = getNanoseconds();
+ // Measure CPU load.
+ int64_t currentTimeNanos = getNanoseconds();
+ // Sometimes we get a short callback when doing sample rate conversion.
+ // Just ignore those to avoid noise.
+ if (numFrames > (getFramesPerCallback() / 2)) {
+ int64_t calculationTime = currentTimeNanos - startTimeNanos;
+ float currentCpuLoad = calculationTime * 0.000000001f * audioStream->getSampleRate() / numFrames;
+ mCpuLoad = (mCpuLoad * 0.95f) + (currentCpuLoad * 0.05f); // simple low pass filter
+ mMaxCpuLoad = std::max(currentCpuLoad, mMaxCpuLoad.load());
+ }
if (mPreviousCallbackTimeNs != 0) {
- mStatistics.add((currentTimeNs - mPreviousCallbackTimeNs) * kNsToMsScaler);
+ mStatistics.add((currentTimeNanos - mPreviousCallbackTimeNs) * kNsToMsScaler);
}
- mPreviousCallbackTimeNs = currentTimeNs;
+ mPreviousCallbackTimeNs = currentTimeNanos;
return callbackResult;
}
+
+int OboeStreamCallbackProxy::applyCpuAffinityMask(uint32_t mask) {
+ int err = 0;
+ // Capture original CPU set so we can restore it.
+ if (!mIsOriginalCpuSetValid) {
+ err = sched_getaffinity((pid_t) 0,
+ sizeof(mOriginalCpuSet),
+ &mOriginalCpuSet);
+ if (err) {
+ LOGE("%s(0x%02X) - sched_getaffinity(), errno = %d\n", __func__, mask, errno);
+ return -errno;
+ }
+ mIsOriginalCpuSetValid = true;
+ }
+ if (mask) {
+ cpu_set_t cpu_set;
+ CPU_ZERO(&cpu_set);
+ int cpuCount = sysconf(_SC_NPROCESSORS_CONF);
+ for (int cpuIndex = 0; cpuIndex < cpuCount; cpuIndex++) {
+ if (mask & (1 << cpuIndex)) {
+ CPU_SET(cpuIndex, &cpu_set);
+ }
+ }
+ err = sched_setaffinity((pid_t) 0, sizeof(cpu_set_t), &cpu_set);
+ } else {
+ // Restore original mask.
+ err = sched_setaffinity((pid_t) 0, sizeof(mOriginalCpuSet), &mOriginalCpuSet);
+ }
+ if (err) {
+ LOGE("%s(0x%02X) - sched_setaffinity(), errno = %d\n", __func__, mask, errno);
+ return -errno;
+ }
+ return 0;
+}
diff --git a/apps/OboeTester/app/src/main/cpp/OboeStreamCallbackProxy.h b/apps/OboeTester/app/src/main/cpp/OboeStreamCallbackProxy.h
index 3d1c8977..157c0817 100644
--- a/apps/OboeTester/app/src/main/cpp/OboeStreamCallbackProxy.h
+++ b/apps/OboeTester/app/src/main/cpp/OboeStreamCallbackProxy.h
@@ -19,8 +19,12 @@
#include <unistd.h>
#include <sys/types.h>
+#include <sys/sysinfo.h>
#include "oboe/Oboe.h"
+#include "synth/Synthesizer.h"
+#include "synth/SynthTools.h"
+#include "OboeTesterStreamCallback.h"
class DoubleStatistics {
public:
@@ -65,12 +69,79 @@ private:
std::atomic<double> maximum { 0 };
};
-class OboeStreamCallbackProxy : public oboe::AudioStreamCallback {
+/**
+ * Manage the synthesizer workload that burdens the CPU.
+ * Adjust the number of voices according to the requested workload.
+ * Trigger noteOn and noteOff messages.
+ */
+class SynthWorkload {
+public:
+ SynthWorkload() {
+ mSynth.setup(marksynth::kSynthmarkSampleRate, marksynth::kSynthmarkMaxVoices);
+ }
+
+ void onCallback(double workload) {
+ // If workload changes then restart notes.
+ if (workload != mPreviousWorkload) {
+ mSynth.allNotesOff();
+ mAreNotesOn = false;
+ mCountdown = 0; // trigger notes on
+ mPreviousWorkload = workload;
+ }
+ if (mCountdown <= 0) {
+ if (mAreNotesOn) {
+ mSynth.allNotesOff();
+ mAreNotesOn = false;
+ mCountdown = mOffFrames;
+ } else {
+ mSynth.notesOn((int)mPreviousWorkload);
+ mAreNotesOn = true;
+ mCountdown = mOnFrames;
+ }
+ }
+ }
+
+ /**
+ * Render the notes into a stereo buffer.
+ * Passing a nullptr will cause the calculated results to be discarded.
+ * The workload should be the same.
+ * @param buffer a real stereo buffer or nullptr
+ * @param numFrames
+ */
+ void renderStereo(float *buffer, int numFrames) {
+ if (buffer == nullptr) {
+ int framesLeft = numFrames;
+ while (framesLeft > 0) {
+ int framesThisTime = std::min(kDummyBufferSizeInFrames, framesLeft);
+ // Do the work then throw it away.
+ mSynth.renderStereo(&mDummyStereoBuffer[0], framesThisTime);
+ framesLeft -= framesThisTime;
+ }
+ } else {
+ mSynth.renderStereo(buffer, numFrames);
+ }
+ mCountdown -= numFrames;
+ }
+
+private:
+ marksynth::Synthesizer mSynth;
+ static constexpr int kDummyBufferSizeInFrames = 32;
+ float mDummyStereoBuffer[kDummyBufferSizeInFrames * 2];
+ double mPreviousWorkload = 1.0;
+ bool mAreNotesOn = false;
+ int mCountdown = 0;
+ int mOnFrames = (int) (0.2 * 48000);
+ int mOffFrames = (int) (0.3 * 48000);
+};
+
+class OboeStreamCallbackProxy : public OboeTesterStreamCallback {
public:
- void setCallback(oboe::AudioStreamCallback *callback) {
+
+ void setDataCallback(oboe::AudioStreamDataCallback *callback) {
mCallback = callback;
setCallbackCount(0);
mStatistics.clear();
+ mPreviousMask = 0;
}
static void setCallbackReturnStop(bool b) {
@@ -100,39 +171,98 @@ public:
/**
* Specify the amount of artificial workload that will waste CPU cycles
* and increase the CPU load.
- * @param workload typically ranges from 0.0 to 100.0
+ * @param workload typically ranges from 0 to 400
*/
- void setWorkload(double workload) {
- mWorkload = std::max(0.0, workload);
+ void setWorkload(int32_t workload) {
+ mNumWorkloadVoices = std::max(0, workload);
+ }
+
+ int32_t getWorkload() const {
+ return mNumWorkloadVoices;
}
- double getWorkload() const {
- return mWorkload;
+ void setHearWorkload(bool enabled) {
+ mHearWorkload = enabled;
}
- double getCpuLoad() const {
+ /**
+ * This is the callback duration relative to the real-time equivalent.
+ * So it may be higher than 1.0.
+ * @return low pass filtered value for the fractional CPU load
+ */
+ float getCpuLoad() const {
return mCpuLoad;
}
+ /**
+ * Calling this will atomically reset the max to zero so only call
+ * this from one client.
+ *
+ * @return last value of the maximum unfiltered CPU load.
+ */
+ float getAndResetMaxCpuLoad() {
+ return mMaxCpuLoad.exchange(0.0f);
+ }
+
std::string getCallbackTimeString() const {
return mStatistics.dump();
}
- static int64_t getNanoseconds(clockid_t clockId = CLOCK_MONOTONIC);
+ /**
+ * @return mask of the CPUs used since the last reset
+ */
+ uint32_t getAndResetCpuMask() {
+ return mCpuMask.exchange(0);
+ }
+ void orCurrentCpuMask(int cpuIndex) {
+ mCpuMask |= (1 << cpuIndex);
+ }
+
+ /**
+ * @param cpuIndex
+ * @return 0 on success or a negative errno
+ */
+ int setCpuAffinity(int cpuIndex) {
+ cpu_set_t cpu_set;
+ CPU_ZERO(&cpu_set);
+ CPU_SET(cpuIndex, &cpu_set);
+ int err = sched_setaffinity((pid_t) 0, sizeof(cpu_set_t), &cpu_set);
+ return err == 0 ? 0 : -errno;
+ }
+
+ /**
+ *
+ * @param mask bits for each CPU or zero for all
+ * @return
+ */
+ int applyCpuAffinityMask(uint32_t mask);
+
+ void setCpuAffinityMask(uint32_t mask) {
+ mCpuAffinityMask = mask;
+ }
private:
- static constexpr int32_t kWorkloadScaler = 500;
static constexpr double kNsToMsScaler = 0.000001;
- double mWorkload = 0.0;
- std::atomic<double> mCpuLoad{0};
+ std::atomic<float> mCpuLoad{0.0f};
+ std::atomic<float> mMaxCpuLoad{0.0f};
int64_t mPreviousCallbackTimeNs = 0;
DoubleStatistics mStatistics;
+ int32_t mNumWorkloadVoices = 0;
+ SynthWorkload mSynthWorkload;
+ bool mHearWorkload = false;
- oboe::AudioStreamCallback *mCallback = nullptr;
+ oboe::AudioStreamDataCallback *mCallback = nullptr;
static bool mCallbackReturnStop;
+
int64_t mCallbackCount = 0;
std::atomic<int32_t> mFramesPerCallback{0};
-};
+ std::atomic<uint32_t> mCpuAffinityMask{0};
+ std::atomic<uint32_t> mPreviousMask{0};
+ std::atomic<uint32_t> mCpuMask{0};
+ cpu_set_t mOriginalCpuSet;
+ bool mIsOriginalCpuSetValid = false;
+
+};
#endif //NATIVEOBOE_OBOESTREAMCALLBACKPROXY_H
diff --git a/apps/OboeTester/app/src/main/cpp/OboeTesterStreamCallback.cpp b/apps/OboeTester/app/src/main/cpp/OboeTesterStreamCallback.cpp
index 583c9846..5109f67c 100644
--- a/apps/OboeTester/app/src/main/cpp/OboeTesterStreamCallback.cpp
+++ b/apps/OboeTester/app/src/main/cpp/OboeTesterStreamCallback.cpp
@@ -14,13 +14,18 @@
* limitations under the License.
*/
+#include <sched.h>
+#include <cstring>
#include "AudioStreamGateway.h"
-#include "oboe/Oboe.h"
#include "common/OboeDebug.h"
-#include <sched.h>
-#include <cstring>
+#include "oboe/Oboe.h"
+#include "OboeStreamCallbackProxy.h"
#include "OboeTesterStreamCallback.h"
+#include "OboeTools.h"
+#include "synth/IncludeMeOnce.h"
+
+int32_t OboeTesterStreamCallback::mHangTimeMillis = 0;
// Print if scheduler changes.
void OboeTesterStreamCallback::printScheduler() {
@@ -38,3 +43,44 @@ void OboeTesterStreamCallback::printScheduler() {
}
#endif
}
+
+// Sleep to cause an XRun. Then reschedule.
+void OboeTesterStreamCallback::maybeHang(const int64_t startNanos) {
+ if (mHangTimeMillis == 0) return;
+
+ if (startNanos > mNextTimeToHang) {
+ LOGD("%s() start sleeping", __func__);
+ // Take short naps until it is time to wake up.
+ int64_t nowNanos = startNanos;
+ int64_t wakeupNanos = startNanos + (mHangTimeMillis * NANOS_PER_MILLISECOND);
+ while (nowNanos < wakeupNanos && mHangTimeMillis > 0) {
+ int32_t sleepTimeMicros = (int32_t) ((wakeupNanos - nowNanos) / 1000);
+ if (sleepTimeMicros == 0) break;
+ // The usleep() function can fail if it sleeps for more than one second.
+ // So sleep for several small intervals.
+ // This also allows us to exit the loop if mHangTimeMillis gets set to zero.
+ const int32_t maxSleepTimeMicros = 100 * 1000;
+ sleepTimeMicros = std::min(maxSleepTimeMicros, sleepTimeMicros);
+ usleep(sleepTimeMicros);
+ nowNanos = getNanoseconds();
+ }
+ // Calculate when we hang again.
+ const int32_t minDurationMillis = 500;
+ const int32_t maxDurationMillis = std::max(10000, mHangTimeMillis * 2);
+ int32_t durationMillis = mHangTimeMillis * 10;
+ durationMillis = std::max(minDurationMillis, std::min(maxDurationMillis, durationMillis));
+ mNextTimeToHang = startNanos + (durationMillis * NANOS_PER_MILLISECOND);
+ LOGD("%s() slept for %d msec, durationMillis = %d", __func__,
+ (int)((nowNanos - startNanos) / 1e6L),
+ durationMillis);
+ }
+}
+
+int64_t OboeTesterStreamCallback::getNanoseconds(clockid_t clockId) {
+ struct timespec time;
+ int result = clock_gettime(clockId, &time);
+ if (result < 0) {
+ return result;
+ }
+ return (time.tv_sec * 1e9) + time.tv_nsec;
+} \ No newline at end of file
diff --git a/apps/OboeTester/app/src/main/cpp/OboeTesterStreamCallback.h b/apps/OboeTester/app/src/main/cpp/OboeTesterStreamCallback.h
index ec01fe5c..70a719ea 100644
--- a/apps/OboeTester/app/src/main/cpp/OboeTesterStreamCallback.h
+++ b/apps/OboeTester/app/src/main/cpp/OboeTesterStreamCallback.h
@@ -19,8 +19,11 @@
#include <unistd.h>
#include <sys/types.h>
+#include <sys/sysinfo.h>
#include "flowgraph/FlowGraphNode.h"
#include "oboe/Oboe.h"
+#include "synth/Synthesizer.h"
+#include "synth/SynthTools.h"
class OboeTesterStreamCallback : public oboe::AudioStreamCallback {
public:
@@ -31,10 +34,24 @@ public:
mPreviousScheduler = -1;
}
+ static int64_t getNanoseconds(clockid_t clockId = CLOCK_MONOTONIC);
+
+ /**
+ * Specify a sleep time that will hang the audio periodically.
+ *
+ * @param hangTimeMillis
+ */
+ static void setHangTimeMillis(int hangTimeMillis) {
+ mHangTimeMillis = hangTimeMillis;
+ }
+
protected:
- void printScheduler();
+ void printScheduler();
+ void maybeHang(int64_t nowNanos);
- int mPreviousScheduler = -1;
+ int mPreviousScheduler = -1;
+ static int mHangTimeMillis;
+ int64_t mNextTimeToHang = 0;
};
diff --git a/apps/OboeTester/app/src/main/cpp/OboeTools.h b/apps/OboeTester/app/src/main/cpp/OboeTools.h
new file mode 100644
index 00000000..c8b47f84
--- /dev/null
+++ b/apps/OboeTester/app/src/main/cpp/OboeTools.h
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef OBOETESTER_OBOETOOLS_H
+#define OBOETESTER_OBOETOOLS_H
+
+#define NANOS_PER_MICROSECOND ((int64_t) 1000)
+#define NANOS_PER_MILLISECOND (1000 * NANOS_PER_MICROSECOND)
+#define NANOS_PER_SECOND (1000 * NANOS_PER_MILLISECOND)
+#define MILLISECONDS_PER_SECOND 1000
+
+#endif //OBOETESTER_OBOETOOLS_H
diff --git a/apps/OboeTester/app/src/main/cpp/TestColdStartLatency.cpp b/apps/OboeTester/app/src/main/cpp/TestColdStartLatency.cpp
new file mode 100644
index 00000000..7de1c7ab
--- /dev/null
+++ b/apps/OboeTester/app/src/main/cpp/TestColdStartLatency.cpp
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <stdlib.h>
+#include <aaudio/AAudioExtensions.h>
+
+#include "common/OboeDebug.h"
+#include "common/AudioClock.h"
+#include "TestColdStartLatency.h"
+#include "OboeTools.h"
+
+using namespace oboe;
+
+int32_t TestColdStartLatency::open(bool useInput, bool useLowLatency, bool useMmap, bool
+ useExclusive) {
+
+ mDataCallback = std::make_shared<MyDataCallback>();
+
+ // Enable MMAP if needed
+ bool wasMMapEnabled = AAudioExtensions::getInstance().isMMapEnabled();
+ AAudioExtensions::getInstance().setMMapEnabled(useMmap);
+
+ int64_t beginOpenNanos = AudioClock::getNanoseconds();
+
+ AudioStreamBuilder builder;
+ Result result = builder.setFormat(AudioFormat::Float)
+ ->setPerformanceMode(useLowLatency ? PerformanceMode::LowLatency :
+ PerformanceMode::None)
+ ->setDirection(useInput ? Direction::Input : Direction::Output)
+ ->setChannelCount(kChannelCount)
+ ->setDataCallback(mDataCallback)
+ ->setSharingMode(useExclusive ? SharingMode::Exclusive : SharingMode::Shared)
+ ->openStream(mStream);
+
+ int64_t endOpenNanos = AudioClock::getNanoseconds();
+ int64_t actualDurationNanos = endOpenNanos - beginOpenNanos;
+ mOpenTimeMicros = actualDurationNanos / NANOS_PER_MICROSECOND;
+
+ // Revert MMAP back to its previous state
+ AAudioExtensions::getInstance().setMMapEnabled(wasMMapEnabled);
+
+ mDeviceId = mStream->getDeviceId();
+
+ return (int32_t) result;
+}
+
+int32_t TestColdStartLatency::start() {
+ mBeginStartNanos = AudioClock::getNanoseconds();
+ Result result = mStream->requestStart();
+ int64_t endStartNanos = AudioClock::getNanoseconds();
+ int64_t actualDurationNanos = endStartNanos - mBeginStartNanos;
+ mStartTimeMicros = actualDurationNanos / NANOS_PER_MICROSECOND;
+ return (int32_t) result;
+}
+
+int32_t TestColdStartLatency::close() {
+ Result result1 = mStream->requestStop();
+ Result result2 = mStream->close();
+ return (int32_t)((result1 != Result::OK) ? result1 : result2);
+}
+
+int32_t TestColdStartLatency::getColdStartTimeMicros() {
+ int64_t position;
+ int64_t timestampNanos;
+ if (mStream->getDirection() == Direction::Output) {
+ auto result = mStream->getTimestamp(CLOCK_MONOTONIC);
+ if (!result) {
+ return -1; // ERROR
+ }
+ auto frameTimestamp = result.value();
+ // Calculate the time that frame[0] would have been played by the speaker.
+ position = frameTimestamp.position;
+ timestampNanos = frameTimestamp.timestamp;
+ } else {
+ position = mStream->getFramesRead();
+ timestampNanos = AudioClock::getNanoseconds();
+ }
+ double sampleRate = (double) mStream->getSampleRate();
+
+ int64_t elapsedNanos = NANOS_PER_SECOND * (position / sampleRate);
+ int64_t timeOfFrameZero = timestampNanos - elapsedNanos;
+ int64_t coldStartLatencyNanos = timeOfFrameZero - mBeginStartNanos;
+ return coldStartLatencyNanos / NANOS_PER_MICROSECOND;
+}
+
+// Callback that sleeps then touches the audio buffer.
+DataCallbackResult TestColdStartLatency::MyDataCallback::onAudioReady(
+ AudioStream *audioStream,
+ void *audioData,
+ int32_t numFrames) {
+ float *floatData = (float *) audioData;
+ const int numSamples = numFrames * kChannelCount;
+ if (audioStream->getDirection() == Direction::Output) {
+ // Fill mono buffer with a sine wave.
+ for (int i = 0; i < numSamples; i++) {
+ *floatData++ = sinf(mPhase) * 0.2f;
+ if ((i % kChannelCount) == (kChannelCount - 1)) {
+ mPhase += kPhaseIncrement;
+ // Wrap the phase around in a circle.
+ if (mPhase >= M_PI) mPhase -= 2 * M_PI;
+ }
+ }
+ }
+ return DataCallbackResult::Continue;
+}
diff --git a/apps/OboeTester/app/src/main/cpp/TestColdStartLatency.h b/apps/OboeTester/app/src/main/cpp/TestColdStartLatency.h
new file mode 100644
index 00000000..7c70687e
--- /dev/null
+++ b/apps/OboeTester/app/src/main/cpp/TestColdStartLatency.h
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef OBOETESTER_TEST_COLD_START_LATENCY_H
+#define OBOETESTER_TEST_COLD_START_LATENCY_H
+
+#include "oboe/Oboe.h"
+#include <thread>
+
+/**
+ * Test for getting the cold start latency
+ */
+class TestColdStartLatency {
+public:
+
+ int32_t open(bool useInput, bool useLowLatency, bool useMmap, bool useExclusive);
+ int32_t start();
+ int32_t close();
+
+ int32_t getColdStartTimeMicros();
+
+ int32_t getOpenTimeMicros() {
+ return (int32_t) (mOpenTimeMicros.load());
+ }
+
+ int32_t getStartTimeMicros() {
+ return (int32_t) (mStartTimeMicros.load());
+ }
+
+ int32_t getDeviceId() {
+ return mDeviceId;
+ }
+
+protected:
+ std::atomic<int64_t> mBeginStartNanos{0};
+ std::atomic<double> mOpenTimeMicros{0};
+ std::atomic<double> mStartTimeMicros{0};
+ std::atomic<double> mColdStartTimeMicros{0};
+ std::atomic<int32_t> mDeviceId{0};
+
+private:
+
+ class MyDataCallback : public oboe::AudioStreamDataCallback { public:
+
+ MyDataCallback() {}
+
+ oboe::DataCallbackResult onAudioReady(
+ oboe::AudioStream *audioStream,
+ void *audioData,
+ int32_t numFrames) override;
+ private:
+ // For sine generator.
+ float mPhase = 0.0f;
+ static constexpr float kPhaseIncrement = 2.0f * (float) M_PI * 440.0f / 48000.0f;
+ };
+
+ std::shared_ptr<oboe::AudioStream> mStream;
+ std::shared_ptr<MyDataCallback> mDataCallback;
+
+ static constexpr int kChannelCount = 1;
+};
+
+#endif //OBOETESTER_TEST_COLD_START_LATENCY_H
diff --git a/apps/OboeTester/app/src/main/cpp/TestRoutingCrash.cpp b/apps/OboeTester/app/src/main/cpp/TestRoutingCrash.cpp
new file mode 100644
index 00000000..633e1186
--- /dev/null
+++ b/apps/OboeTester/app/src/main/cpp/TestRoutingCrash.cpp
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <stdlib.h>
+#include <aaudio/AAudioExtensions.h>
+
+#include "common/OboeDebug.h"
+#include "common/AudioClock.h"
+#include "TestRoutingCrash.h"
+
+using namespace oboe;
+
+// open start start an Oboe stream
+int32_t TestRoutingCrash::start(bool useInput) {
+
+ mDataCallback = std::make_shared<MyDataCallback>(this);
+
+ // Disable MMAP because we are trying to crash a Legacy Stream.
+ bool wasMMapEnabled = AAudioExtensions::getInstance().isMMapEnabled();
+ AAudioExtensions::getInstance().setMMapEnabled(false);
+
+ AudioStreamBuilder builder;
+ oboe::Result result = builder.setFormat(oboe::AudioFormat::Float)
+#if 1
+ ->setPerformanceMode(oboe::PerformanceMode::LowLatency)
+#else
+ ->setPerformanceMode(oboe::PerformanceMode::None)
+#endif
+ ->setDirection(useInput ? oboe::Direction::Input : oboe::Direction::Output)
+ ->setChannelCount(kChannelCount)
+ ->setDataCallback(mDataCallback)
+ // Use VoiceCommunication so we can reroute it by setting SpeakerPhone ON/OFF.
+ ->setUsage(oboe::Usage::VoiceCommunication)
+ ->openStream(mStream);
+ if (result != oboe::Result::OK) {
+ return (int32_t) result;
+ }
+
+ AAudioExtensions::getInstance().setMMapEnabled(wasMMapEnabled);
+ return (int32_t) mStream->requestStart();
+}
+
+int32_t TestRoutingCrash::stop() {
+ oboe::Result result1 = mStream->requestStop();
+ oboe::Result result2 = mStream->close();
+ return (int32_t)((result1 != oboe::Result::OK) ? result1 : result2);
+}
+
+// Callback that sleeps then touches the audio buffer.
+DataCallbackResult TestRoutingCrash::MyDataCallback::onAudioReady(
+ AudioStream *audioStream,
+ void *audioData,
+ int32_t numFrames) {
+ float *floatData = (float *) audioData;
+
+ // If I call getTimestamp() here it does NOT crash!
+
+ // Simulate the timing of a heavy workload by sleeping.
+ // Otherwise the window for the crash is very narrow.
+ const double kDutyCycle = 0.7;
+ const double bufferTimeNanos = 1.0e9 * numFrames / (double) audioStream->getSampleRate();
+ const int64_t targetDurationNanos = (int64_t) (bufferTimeNanos * kDutyCycle);
+ if (targetDurationNanos > 0) {
+ AudioClock::sleepForNanos(targetDurationNanos);
+ }
+ const double kFilterCoefficient = 0.95; // low pass IIR filter
+ const double sleepMicros = targetDurationNanos * 0.0001;
+ mParent->averageSleepTimeMicros = ((1.0 - kFilterCoefficient) * sleepMicros)
+ + (kFilterCoefficient * mParent->averageSleepTimeMicros);
+
+ // If I call getTimestamp() here it crashes.
+ audioStream->getTimestamp(CLOCK_MONOTONIC); // Trigger a restoreTrack_l() in framework.
+
+ const int numSamples = numFrames * kChannelCount;
+ if (audioStream->getDirection() == oboe::Direction::Input) {
+ // Read buffer and write sum of samples to a member variable.
+ // We just want to touch the memory and not get optimized away by the compiler.
+ float sum = 0.0f;
+ for (int i = 0; i < numSamples; i++) {
+ sum += *floatData++;
+ }
+ mInputSum = sum;
+ } else {
+ // Fill mono buffer with a sine wave.
+ // If the routing occurred then the buffer may be dead and
+ // we may be writing into unallocated memory.
+ for (int i = 0; i < numSamples; i++) {
+ *floatData++ = sinf(mPhase) * 0.2f;
+ mPhase += kPhaseIncrement;
+ // Wrap the phase around in a circle.
+ if (mPhase >= M_PI) mPhase -= 2 * M_PI;
+ }
+ }
+
+ // If I call getTimestamp() here it does NOT crash!
+
+ return oboe::DataCallbackResult::Continue;
+}
diff --git a/apps/OboeTester/app/src/main/cpp/TestRoutingCrash.h b/apps/OboeTester/app/src/main/cpp/TestRoutingCrash.h
new file mode 100644
index 00000000..bddc24ac
--- /dev/null
+++ b/apps/OboeTester/app/src/main/cpp/TestRoutingCrash.h
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef OBOETESTER_TEST_ROUTING_CRASH_H
+#define OBOETESTER_TEST_ROUTING_CRASH_H
+
+#include "oboe/Oboe.h"
+#include <thread>
+
+/**
+ * Try to cause a crash by changing routing during a data callback.
+ * We use Use::VoiceCommunication for the stream and
+ * setSpeakerPhoneOn(b) to force a routing change.
+ * This works best when connected to a BT headset.
+ */
+class TestRoutingCrash {
+public:
+
+ int32_t start(bool useInput);
+ int32_t stop();
+
+ int32_t getSleepTimeMicros() {
+ return (int32_t) (averageSleepTimeMicros.load());
+ }
+
+protected:
+
+ std::atomic<double> averageSleepTimeMicros{0};
+
+private:
+
+ class MyDataCallback : public oboe::AudioStreamDataCallback { public:
+
+ MyDataCallback(TestRoutingCrash *parent): mParent(parent) {}
+
+ oboe::DataCallbackResult onAudioReady(
+ oboe::AudioStream *audioStream,
+ void *audioData,
+ int32_t numFrames) override;
+ private:
+ TestRoutingCrash *mParent;
+ // For sine generator.
+ float mPhase = 0.0f;
+ static constexpr float kPhaseIncrement = 2.0f * (float) M_PI * 440.0f / 48000.0f;
+ float mInputSum = 0.0f; // For saving input data sum to prevent over-optimization.
+ };
+
+ std::shared_ptr<oboe::AudioStream> mStream;
+ std::shared_ptr<MyDataCallback> mDataCallback;
+
+ static constexpr int kChannelCount = 1;
+};
+
+#endif //OBOETESTER_TEST_ROUTING_CRASH_H
diff --git a/apps/OboeTester/app/src/main/cpp/analyzer/BaseSineAnalyzer.h b/apps/OboeTester/app/src/main/cpp/analyzer/BaseSineAnalyzer.h
index 37e13430..7c7fc80f 100644
--- a/apps/OboeTester/app/src/main/cpp/analyzer/BaseSineAnalyzer.h
+++ b/apps/OboeTester/app/src/main/cpp/analyzer/BaseSineAnalyzer.h
@@ -38,15 +38,15 @@ public:
: LoopbackProcessor()
, mInfiniteRecording(64 * 1024) {}
-
virtual bool isOutputEnabled() { return true; }
void setMagnitude(double magnitude) {
mMagnitude = magnitude;
- mScaledTolerance = mMagnitude * mTolerance;
+ mScaledTolerance = mMagnitude * getTolerance();
}
double getPhaseOffset() {
+ ALOGD("%s(), mPhaseOffset = %f\n", __func__, mPhaseOffset);
return mPhaseOffset;
}
@@ -106,7 +106,7 @@ public:
incrementOutputPhase();
output = (sinOut * mOutputAmplitude)
+ (mWhiteNoise.nextRandomDouble() * getNoiseAmplitude());
- // ALOGD("sin(%f) = %f, %f\n", mOutputPhase, sinOut, mPhaseIncrement);
+ // ALOGD("sin(%f) = %f, %f\n", mOutputPhase, sinOut, kPhaseIncrement);
}
for (int i = 0; i < channelCount; i++) {
frameData[i] = (i == mOutputChannel) ? output : 0.0f;
@@ -129,7 +129,7 @@ public:
double cosMean = mCosAccumulator / mFramesAccumulated;
double magnitude = 2.0 * sqrt((sinMean * sinMean) + (cosMean * cosMean));
if (phasePtr != nullptr) {
- double phase = M_PI_2 - atan2(sinMean, cosMean);
+ double phase = atan2(cosMean, sinMean);
*phasePtr = phase;
}
return magnitude;
@@ -138,6 +138,7 @@ public:
/**
* Perform sin/cos analysis on each sample.
* Measure magnitude and phase on every period.
+ * Updates mPhaseOffset
* @param sample
* @param referencePhase
* @return true if magnitude and phase updated
@@ -152,8 +153,10 @@ public:
if (mFramesAccumulated == mSinePeriod) {
const double coefficient = 0.1;
double magnitude = calculateMagnitudePhase(&mPhaseOffset);
+ ALOGD("%s(), mPhaseOffset = %f\n", __func__, mPhaseOffset);
// One pole averaging filter.
setMagnitude((mMagnitude * (1.0 - coefficient)) + (magnitude * coefficient));
+ resetAccumulator();
return true;
} else {
return false;
@@ -182,7 +185,8 @@ public:
}
protected:
- static constexpr int32_t kTargetGlitchFrequency = 1000;
+ // Try to get a prime period so the waveform plot changes every time.
+ static constexpr int32_t kTargetGlitchFrequency = 48000 / 113;
int32_t mSinePeriod = 1; // this will be set before use
double mInverseSinePeriod = 1.0;
diff --git a/apps/OboeTester/app/src/main/cpp/analyzer/DataPathAnalyzer.h b/apps/OboeTester/app/src/main/cpp/analyzer/DataPathAnalyzer.h
index 5814dccd..953cf49a 100644
--- a/apps/OboeTester/app/src/main/cpp/analyzer/DataPathAnalyzer.h
+++ b/apps/OboeTester/app/src/main/cpp/analyzer/DataPathAnalyzer.h
@@ -41,6 +41,18 @@ public:
setNoiseAmplitude(0.02);
}
+ double calculatePhaseError(double p1, double p2) {
+ double diff = p1 - p2;
+ // Wrap around the circle.
+ while (diff > M_PI) {
+ diff -= (2 * M_PI);
+ }
+ while (diff < -M_PI) {
+ diff += (2 * M_PI);
+ }
+ return diff;
+ }
+
/**
* @param frameData contains microphone data with sine signal feedback
* @param channelCount
@@ -52,9 +64,8 @@ public:
mInfiniteRecording.write(sample);
if (transformSample(sample, mOutputPhase)) {
- resetAccumulator();
// Analyze magnitude and phase on every period.
- double diff = abs(mPhaseOffset - mPreviousPhaseOffset);
+ double diff = fabs(calculatePhaseError(mPhaseOffset, mPreviousPhaseOffset));
if (diff < mPhaseTolerance) {
mMaxMagnitude = std::max(mMagnitude, mMaxMagnitude);
}
diff --git a/apps/OboeTester/app/src/main/cpp/analyzer/GlitchAnalyzer.h b/apps/OboeTester/app/src/main/cpp/analyzer/GlitchAnalyzer.h
index 52b00139..747c5ea9 100644
--- a/apps/OboeTester/app/src/main/cpp/analyzer/GlitchAnalyzer.h
+++ b/apps/OboeTester/app/src/main/cpp/analyzer/GlitchAnalyzer.h
@@ -50,10 +50,18 @@ public:
return mMagnitude;
}
+ int getSinePeriod() const {
+ return mSinePeriod;
+ }
+
int32_t getGlitchCount() const {
return mGlitchCount;
}
+ int32_t getGlitchLength() const {
+ return mGlitchLength;
+ }
+
int32_t getStateFrameCount(int state) const {
return mStateFrameCounters[state];
}
@@ -63,7 +71,7 @@ public:
if (mState != STATE_LOCKED
|| mMeanSquareSignal < threshold
|| mMeanSquareNoise < threshold) {
- return 0.0;
+ return -999.0; // error indicator
} else {
double signalToNoise = mMeanSquareSignal / mMeanSquareNoise; // power ratio
double signalToNoiseDB = 10.0 * log(signalToNoise);
@@ -124,21 +132,22 @@ public:
result_code result = RESULT_OK;
float sample = frameData[getInputChannel()];
- float peak = mPeakFollower.process(sample);
- mInfiniteRecording.write(sample);
// Force a periodic glitch to test the detector!
- if (mForceGlitchDuration > 0) {
+ if (mForceGlitchDurationFrames > 0) {
if (mForceGlitchCounter == 0) {
- ALOGE("%s: force a glitch!!", __func__);
- mForceGlitchCounter = getSampleRate();
- } else if (mForceGlitchCounter <= mForceGlitchDuration) {
+ ALOGE("%s: finish a glitch!!", __func__);
+ mForceGlitchCounter = kForceGlitchPeriod;
+ } else if (mForceGlitchCounter <= mForceGlitchDurationFrames) {
// Force an abrupt offset.
- sample += (sample > 0.0) ? -0.5f : 0.5f;
+ sample += (sample > 0.0) ? -kForceGlitchOffset : kForceGlitchOffset;
}
--mForceGlitchCounter;
}
+ float peak = mPeakFollower.process(sample);
+ mInfiniteRecording.write(sample);
+
mStateFrameCounters[mState]++; // count how many frames we are in each state
switch (mState) {
@@ -175,11 +184,12 @@ public:
// Must be a multiple of the period or the calculation will not be accurate.
if (mFramesAccumulated == mSinePeriod * PERIODS_NEEDED_FOR_LOCK) {
setMagnitude(calculateMagnitudePhase(&mPhaseOffset));
-// ALOGD("%s() mag = %f, offset = %f, prev = %f",
-// __func__, mMagnitude, mPhaseOffset, mPreviousPhaseOffset);
+ ALOGD("%s() mag = %f, mPhaseOffset = %f",
+ __func__, mMagnitude, mPhaseOffset);
if (mMagnitude > mThreshold) {
- if (abs(mPhaseOffset) < kMaxPhaseError) {
+ if (fabs(mPhaseOffset) < kMaxPhaseError) {
mState = STATE_LOCKED;
+ mConsecutiveBadFrames = 0;
// ALOGD("%5d: switch to STATE_LOCKED", mFrameCounter);
}
// Adjust mInputPhase to match measured phase
@@ -196,24 +206,36 @@ public:
double diff = predicted - sample;
double absDiff = fabs(diff);
mMaxGlitchDelta = std::max(mMaxGlitchDelta, absDiff);
- if (absDiff > mScaledTolerance) {
- result = ERROR_GLITCHES;
- onGlitchStart();
-// LOGI("diff glitch detected, absDiff = %g", absDiff);
- } else {
+ if (absDiff > mScaledTolerance) { // bad frame
+ mConsecutiveBadFrames++;
+ mConsecutiveGoodFrames = 0;
+ LOGI("diff glitch frame #%d detected, absDiff = %g > %g",
+ mConsecutiveBadFrames, absDiff, mScaledTolerance);
+ if (mConsecutiveBadFrames > 0) {
+ result = ERROR_GLITCHES;
+ onGlitchStart();
+ }
+ resetAccumulator();
+ } else { // good frame
+ mConsecutiveBadFrames = 0;
+ mConsecutiveGoodFrames++;
+
mSumSquareSignal += predicted * predicted;
mSumSquareNoise += diff * diff;
-
// Track incoming signal and slowly adjust magnitude to account
// for drift in the DRC or AGC.
// Must be a multiple of the period or the calculation will not be accurate.
if (transformSample(sample, mInputPhase)) {
+ // Adjust phase to account for sample rate drift.
+ mInputPhase += mPhaseOffset;
+
mMeanSquareNoise = mSumSquareNoise * mInverseSinePeriod;
mMeanSquareSignal = mSumSquareSignal * mInverseSinePeriod;
- resetAccumulator();
+ mSumSquareNoise = 0.0;
+ mSumSquareSignal = 0.0;
- if (abs(mPhaseOffset) > kMaxPhaseError) {
+ if (fabs(mPhaseOffset) > kMaxPhaseError) {
result = ERROR_GLITCHES;
onGlitchStart();
ALOGD("phase glitch detected, phaseOffset = %g", mPhaseOffset);
@@ -229,22 +251,25 @@ public:
case STATE_GLITCHING: {
// Predict next sine value
- mGlitchLength++;
double predicted = sinf(mInputPhase) * mMagnitude;
double diff = predicted - sample;
double absDiff = fabs(diff);
mMaxGlitchDelta = std::max(mMaxGlitchDelta, absDiff);
- if (absDiff < mScaledTolerance) { // close enough?
- // If we get a full sine period of non-glitch samples in a row then consider the glitch over.
+ if (absDiff > mScaledTolerance) { // bad frame
+ mConsecutiveBadFrames++;
+ mConsecutiveGoodFrames = 0;
+ mGlitchLength++;
+ if (mGlitchLength > maxMeasurableGlitchLength()) {
+ onGlitchTerminated();
+ }
+ } else { // good frame
+ mConsecutiveBadFrames = 0;
+ mConsecutiveGoodFrames++;
+ // If we get a full sine period of good samples in a row then consider the glitch over.
// We don't want to just consider a zero crossing the end of a glitch.
- if (mNonGlitchCount++ > mSinePeriod) {
+ if (mConsecutiveGoodFrames > mSinePeriod) {
onGlitchEnd();
}
- } else {
- mNonGlitchCount = 0;
- if (mGlitchLength > (4 * mSinePeriod)) {
- relock();
- }
}
incrementInputPhase();
} break;
@@ -258,6 +283,8 @@ public:
return result;
}
+ int maxMeasurableGlitchLength() const { return 2 * mSinePeriod; }
+
// advance and wrap phase
void incrementInputPhase() {
mInputPhase += mPhaseIncrement;
@@ -269,16 +296,29 @@ public:
bool isOutputEnabled() override { return mState != STATE_IDLE; }
void onGlitchStart() {
- mGlitchCount++;
-// ALOGD("%5d: STARTED a glitch # %d", mFrameCounter, mGlitchCount);
mState = STATE_GLITCHING;
mGlitchLength = 1;
- mNonGlitchCount = 0;
mLastGlitchPosition = mInfiniteRecording.getTotalWritten();
+ ALOGD("%5d: STARTED a glitch # %d, pos = %5d",
+ mFrameCounter, mGlitchCount, (int)mLastGlitchPosition);
+ ALOGD("glitch mSinePeriod = %d", mSinePeriod);
+ }
+
+ /**
+ * Give up waiting for a glitch to end and try to resync.
+ */
+ void onGlitchTerminated() {
+ mGlitchCount++;
+ ALOGD("%5d: TERMINATED a glitch # %d, length = %d", mFrameCounter, mGlitchCount, mGlitchLength);
+ // We don't know how long the glitch really is so set the length to -1.
+ mGlitchLength = -1;
+ mState = STATE_WAITING_FOR_LOCK;
+ resetAccumulator();
}
void onGlitchEnd() {
-// ALOGD("%5d: ENDED a glitch # %d, length = %d", mFrameCounter, mGlitchCount, mGlitchLength);
+ mGlitchCount++;
+ ALOGD("%5d: ENDED a glitch # %d, length = %d", mFrameCounter, mGlitchCount, mGlitchLength);
mState = STATE_LOCKED;
resetAccumulator();
}
@@ -286,14 +326,6 @@ public:
// reset the sine wave detector
void resetAccumulator() override {
BaseSineAnalyzer::resetAccumulator();
- mSumSquareSignal = 0.0;
- mSumSquareNoise = 0.0;
- }
-
- void relock() {
-// ALOGD("relock: %d because of a very long %d glitch", mFrameCounter, mGlitchLength);
- mState = STATE_WAITING_FOR_LOCK;
- resetAccumulator();
}
void reset() override {
@@ -305,6 +337,7 @@ public:
void prepareToTest() override {
BaseSineAnalyzer::prepareToTest();
mGlitchCount = 0;
+ mGlitchLength = 0;
mMaxGlitchDelta = 0.0;
for (int i = 0; i < NUM_STATES; i++) {
mStateFrameCounters[i] = 0;
@@ -312,7 +345,26 @@ public:
}
int32_t getLastGlitch(float *buffer, int32_t length) {
- return mInfiniteRecording.readFrom(buffer, mLastGlitchPosition - 32, length);
+ const int margin = mSinePeriod;
+ int32_t numSamples = mInfiniteRecording.readFrom(buffer,
+ mLastGlitchPosition - margin,
+ length);
+ ALOGD("%s: glitch at %d, edge = %7.4f, %7.4f, %7.4f",
+ __func__, (int)mLastGlitchPosition,
+ buffer[margin - 1], buffer[margin], buffer[margin+1]);
+ return numSamples;
+ }
+
+ int32_t getRecentSamples(float *buffer, int32_t length) {
+ int firstSample = mInfiniteRecording.getTotalWritten() - length;
+ int32_t numSamples = mInfiniteRecording.readFrom(buffer,
+ firstSample,
+ length);
+ return numSamples;
+ }
+
+ void setForcedGlitchDuration(int frames) {
+ mForceGlitchDurationFrames = frames;
}
private:
@@ -347,13 +399,16 @@ private:
double mInputPhase = 0.0;
double mMaxGlitchDelta = 0.0;
int32_t mGlitchCount = 0;
- int32_t mNonGlitchCount = 0;
+ int32_t mConsecutiveBadFrames = 0;
+ int32_t mConsecutiveGoodFrames = 0;
int32_t mGlitchLength = 0;
int mDownCounter = IDLE_FRAME_COUNT;
int32_t mFrameCounter = 0;
- int32_t mForceGlitchDuration = 0; // if > 0 then force a glitch for debugging
- int32_t mForceGlitchCounter = 4 * 48000; // count down and trigger at zero
+ int32_t mForceGlitchDurationFrames = 0; // if > 0 then force a glitch for debugging
+ static constexpr int32_t kForceGlitchPeriod = 2 * 48000; // How often we glitch
+ static constexpr float kForceGlitchOffset = 0.20f;
+ int32_t mForceGlitchCounter = kForceGlitchPeriod; // count down and trigger at zero
// measure background noise continuously as a deviation from the expected signal
double mSumSquareSignal = 0.0;
diff --git a/apps/OboeTester/app/src/main/cpp/analyzer/InfiniteRecording.h b/apps/OboeTester/app/src/main/cpp/analyzer/InfiniteRecording.h
index c02c0025..c12348c0 100644
--- a/apps/OboeTester/app/src/main/cpp/analyzer/InfiniteRecording.h
+++ b/apps/OboeTester/app/src/main/cpp/analyzer/InfiniteRecording.h
@@ -34,6 +34,7 @@ public:
int32_t readFrom(T *buffer, size_t position, size_t count) {
const size_t maxPosition = mWritten.load();
position = std::min(position, maxPosition);
+
size_t numToRead = std::min(count, mMaxSamples);
numToRead = std::min(numToRead, maxPosition - position);
if (numToRead == 0) return 0;
@@ -61,7 +62,7 @@ public:
private:
std::unique_ptr<T[]> mData;
- std::atomic<size_t> mWritten{0};
- const size_t mMaxSamples;
+ std::atomic<size_t> mWritten{0};
+ const size_t mMaxSamples;
};
#endif //OBOETESTER_INFINITE_RECORDING_H
diff --git a/apps/OboeTester/app/src/main/cpp/analyzer/LatencyAnalyzer.h b/apps/OboeTester/app/src/main/cpp/analyzer/LatencyAnalyzer.h
index b920c89d..19f6cb11 100644
--- a/apps/OboeTester/app/src/main/cpp/analyzer/LatencyAnalyzer.h
+++ b/apps/OboeTester/app/src/main/cpp/analyzer/LatencyAnalyzer.h
@@ -48,6 +48,9 @@
#define LOOPBACK_RESULT_TAG "RESULT: "
+// Enable or disable the optimized latency calculation.
+#define USE_FAST_LATENCY_CALCULATION 1
+
static constexpr int32_t kDefaultSampleRate = 48000;
static constexpr int32_t kMillisPerSecond = 1000; // by definition
static constexpr int32_t kMaxLatencyMillis = 1000; // arbitrary and generous
@@ -69,13 +72,14 @@ struct LatencyReport {
static float calculateNormalizedCorrelation(const float *a,
const float *b,
- int windowSize) {
+ int windowSize,
+ int stride) {
float correlation = 0.0;
float sumProducts = 0.0;
float sumSquares = 0.0;
// Correlate a against b.
- for (int i = 0; i < windowSize; i++) {
+ for (int i = 0; i < windowSize; i += stride) {
float s1 = a[i];
float s2 = b[i];
// Use a normalized cross-correlation.
@@ -204,7 +208,7 @@ public:
float normalize(float target) {
float maxValue = 1.0e-9f;
for (int i = 0; i < mFrameCounter; i++) {
- maxValue = std::max(maxValue, abs(mData[i]));
+ maxValue = std::max(maxValue, fabsf(mData[i]));
}
float gain = target / maxValue;
for (int i = 0; i < mFrameCounter; i++) {
@@ -220,32 +224,46 @@ private:
int32_t mSampleRate = kDefaultSampleRate; // common default
};
-static int measureLatencyFromPulse(AudioRecording &recorded,
- AudioRecording &pulse,
- LatencyReport *report) {
-
+/**
+ * Find latency using cross correlation in window of the recorded audio.
+ * The stride is used to skip over samples and reduce the CPU load.
+ */
+static int measureLatencyFromPulsePartial(AudioRecording &recorded,
+ int32_t recordedOffset,
+ int32_t recordedWindowSize,
+ AudioRecording &pulse,
+ LatencyReport *report,
+ int32_t stride) {
report->reset();
- int numCorrelations = recorded.size() - pulse.size();
+ if (recordedOffset + recordedWindowSize + pulse.size() > recorded.size()) {
+ ALOGE("%s() tried to correlate past end of recording, recordedOffset = %d frames\n",
+ __func__, recordedOffset);
+ return -3;
+ }
+
+ int32_t numCorrelations = recordedWindowSize / stride;
if (numCorrelations < 10) {
- ALOGE("%s() recording too small = %d frames\n", __func__, recorded.size());
+ ALOGE("%s() recording too small = %d frames, numCorrelations = %d\n",
+ __func__, recorded.size(), numCorrelations);
return -1;
}
std::unique_ptr<float[]> correlations= std::make_unique<float[]>(numCorrelations);
// Correlate pulse against the recorded data.
- for (int i = 0; i < numCorrelations; i++) {
- float correlation = calculateNormalizedCorrelation(&recorded.getData()[i],
+ for (int32_t i = 0; i < numCorrelations; i++) {
+ const int32_t index = (i * stride) + recordedOffset;
+ float correlation = calculateNormalizedCorrelation(&recorded.getData()[index],
&pulse.getData()[0],
- pulse.size());
+ pulse.size(),
+ stride);
correlations[i] = correlation;
}
-
// Find highest peak in correlation array.
float peakCorrelation = 0.0;
- int peakIndex = -1;
- for (int i = 0; i < numCorrelations; i++) {
- float value = abs(correlations[i]);
+ int32_t peakIndex = -1;
+ for (int32_t i = 0; i < numCorrelations; i++) {
+ float value = fabsf(correlations[i]);
if (value > peakCorrelation) {
peakCorrelation = value;
peakIndex = i;
@@ -258,21 +276,64 @@ static int measureLatencyFromPulse(AudioRecording &recorded,
#if 0
// Dump correlation data for charting.
else {
- const int margin = 50;
- int startIndex = std::max(0, peakIndex - margin);
- int endIndex = std::min(numCorrelations - 1, peakIndex + margin);
- for (int index = startIndex; index < endIndex; index++) {
+ const int32_t margin = 50;
+ int32_t startIndex = std::max(0, peakIndex - margin);
+ int32_t endIndex = std::min(numCorrelations - 1, peakIndex + margin);
+ for (int32_t index = startIndex; index < endIndex; index++) {
ALOGD("Correlation, %d, %f", index, correlations[index]);
}
}
#endif
- report->latencyInFrames = peakIndex;
+ report->latencyInFrames = recordedOffset + (peakIndex * stride);
report->correlation = peakCorrelation;
return 0;
}
+#if USE_FAST_LATENCY_CALCULATION
+static int measureLatencyFromPulse(AudioRecording &recorded,
+ AudioRecording &pulse,
+ LatencyReport *report) {
+ const int32_t coarseStride = 16;
+ const int32_t fineWindowSize = coarseStride * 8;
+ const int32_t fineStride = 1;
+ LatencyReport courseReport;
+ courseReport.reset();
+ // Do a rough search, skipping over most of the samples.
+ int result = measureLatencyFromPulsePartial(recorded,
+ 0, // recordedOffset,
+ recorded.size() - pulse.size(),
+ pulse,
+ &courseReport,
+ coarseStride);
+ if (result != 0) {
+ return result;
+ }
+ // Now do a fine resolution search near the coarse latency result.
+ int32_t recordedOffset = std::max(0, courseReport.latencyInFrames - (fineWindowSize / 2));
+ result = measureLatencyFromPulsePartial(recorded,
+ recordedOffset,
+ fineWindowSize,
+ pulse,
+ report,
+ fineStride );
+ return result;
+}
+#else
+// TODO - When we are confident of the new code we can remove this old code.
+static int measureLatencyFromPulse(AudioRecording &recorded,
+ AudioRecording &pulse,
+ LatencyReport *report) {
+ return measureLatencyFromPulsePartial(recorded,
+ 0,
+ recorded.size() - pulse.size(),
+ pulse,
+ report,
+ 1 );
+}
+#endif
+
// ====================================================================================
class LoopbackProcessor {
public:
@@ -514,7 +575,7 @@ public:
<< latencyMillis << "\n";
report << LOOPBACK_RESULT_TAG "latency.confidence = " << std::setw(8)
<< getMeasuredConfidence() << "\n";
- report << LOOPBACK_RESULT_TAG "latency.correlation = " << std::setw(8)
+ report << LOOPBACK_RESULT_TAG "latency.correlation = " << std::setw(8)
<< getMeasuredCorrelation() << "\n";
}
mState = STATE_DONE;
diff --git a/apps/OboeTester/app/src/main/cpp/analyzer/ManchesterEncoder.h b/apps/OboeTester/app/src/main/cpp/analyzer/ManchesterEncoder.h
index 0a4bd5b2..af1c8430 100644
--- a/apps/OboeTester/app/src/main/cpp/analyzer/ManchesterEncoder.h
+++ b/apps/OboeTester/app/src/main/cpp/analyzer/ManchesterEncoder.h
@@ -45,13 +45,13 @@ public:
/**
* This will be called when the next byte is needed.
- * @return
+ * @return next byte
*/
virtual uint8_t onNextByte() = 0;
/**
* Generate the next floating point sample.
- * @return
+ * @return next float
*/
virtual float nextFloat() {
advanceSample();
@@ -66,7 +66,6 @@ protected:
/**
* This will be called when a new bit is ready to be encoded.
* It can be used to prepare the encoded samples.
- * @param current
*/
virtual void onNextBit(bool /* current */) {};
diff --git a/apps/OboeTester/app/src/main/cpp/jni-bridge.cpp b/apps/OboeTester/app/src/main/cpp/jni-bridge.cpp
index 3b898a82..9cb9ffe6 100644
--- a/apps/OboeTester/app/src/main/cpp/jni-bridge.cpp
+++ b/apps/OboeTester/app/src/main/cpp/jni-bridge.cpp
@@ -20,15 +20,19 @@
#include <cstring>
#include <jni.h>
#include <stdint.h>
+#include <sys/sysinfo.h>
#include <thread>
+#include "common/AdpfWrapper.h"
#include "common/OboeDebug.h"
#include "oboe/Oboe.h"
#include "NativeAudioContext.h"
+#include "TestColdStartLatency.h"
#include "TestErrorCallback.h"
+#include "TestRoutingCrash.h"
-NativeAudioContext engine;
+static NativeAudioContext engine;
/*********************************************************************************/
/********************** JNI Prototypes *****************************************/
@@ -47,6 +51,7 @@ Java_com_mobileer_oboetester_OboeAudioStream_openNative(JNIEnv *env, jobject,
jint inputPreset,
jint usage,
jint contentType,
+ jint bufferCapacityInFrames,
jint deviceId,
jint sessionId,
jboolean channelConversionAllowed,
@@ -84,7 +89,7 @@ Java_com_mobileer_oboetester_OboeAudioOutputStream_trigger(JNIEnv *env, jobject)
JNIEXPORT void JNICALL
Java_com_mobileer_oboetester_OboeAudioOutputStream_setToneType(JNIEnv *env, jobject, jint);
JNIEXPORT void JNICALL
-Java_com_mobileer_oboetester_OboeAudioOutputStream_setAmplitude(JNIEnv *env, jobject, jdouble);
+Java_com_mobileer_oboetester_OboeAudioOutputStream_setAmplitude(JNIEnv *env, jobject, jfloat);
/*********************************************************************************/
/********************** JNI Implementations *************************************/
@@ -113,6 +118,18 @@ Java_com_mobileer_oboetester_NativeEngine_areWorkaroundsEnabled(JNIEnv *env,
}
JNIEXPORT jint JNICALL
+Java_com_mobileer_oboetester_NativeEngine_getCpuCount(JNIEnv *env, jclass type) {
+ return sysconf(_SC_NPROCESSORS_CONF);
+}
+
+JNIEXPORT void JNICALL
+ Java_com_mobileer_oboetester_NativeEngine_setCpuAffinityMask(JNIEnv *env,
+ jclass type,
+ jint mask) {
+ engine.getCurrentActivity()->setCpuAffinityMask(mask);
+}
+
+JNIEXPORT jint JNICALL
Java_com_mobileer_oboetester_OboeAudioStream_openNative(
JNIEnv *env, jobject synth,
jint nativeApi,
@@ -125,6 +142,7 @@ Java_com_mobileer_oboetester_OboeAudioStream_openNative(
jint inputPreset,
jint usage,
jint contentType,
+ jint bufferCapacityInFrames,
jint deviceId,
jint sessionId,
jboolean channelConversionAllowed,
@@ -144,6 +162,7 @@ Java_com_mobileer_oboetester_OboeAudioStream_openNative(
inputPreset,
usage,
contentType,
+ bufferCapacityInFrames,
deviceId,
sessionId,
channelConversionAllowed,
@@ -164,11 +183,21 @@ Java_com_mobileer_oboetester_TestAudioActivity_pauseNative(JNIEnv *env, jobject)
}
JNIEXPORT jint JNICALL
+Java_com_mobileer_oboetester_TestAudioActivity_flushNative(JNIEnv *env, jobject) {
+ return (jint) engine.getCurrentActivity()->flush();
+}
+
+JNIEXPORT jint JNICALL
Java_com_mobileer_oboetester_TestAudioActivity_stopNative(JNIEnv *env, jobject) {
return (jint) engine.getCurrentActivity()->stop();
}
JNIEXPORT jint JNICALL
+Java_com_mobileer_oboetester_TestAudioActivity_releaseNative(JNIEnv *env, jobject) {
+ return (jint) engine.getCurrentActivity()->release();
+}
+
+JNIEXPORT jint JNICALL
Java_com_mobileer_oboetester_TestAudioActivity_getFramesPerCallback(JNIEnv *env, jobject) {
return (jint) engine.getCurrentActivity()->getFramesPerCallback();
}
@@ -183,6 +212,11 @@ Java_com_mobileer_oboetester_OboeAudioStream_close(JNIEnv *env, jobject, jint st
engine.getCurrentActivity()->close(streamIndex);
}
+JNIEXPORT void JNICALL
+Java_com_mobileer_oboetester_TestAudioActivity_setUseAlternativeAdpf(JNIEnv *env, jobject, jboolean enabled) {
+ AdpfWrapper::setUseAlternative(enabled);
+}
+
JNIEXPORT jint JNICALL
Java_com_mobileer_oboetester_OboeAudioStream_setBufferSizeInFrames(
JNIEnv *env, jobject, jint streamIndex, jint threshold) {
@@ -207,6 +241,15 @@ Java_com_mobileer_oboetester_OboeAudioStream_getBufferSizeInFrames(
return result;
}
+JNIEXPORT void JNICALL
+Java_com_mobileer_oboetester_OboeAudioStream_setPerformanceHintEnabled(
+ JNIEnv *env, jobject, jint streamIndex, jboolean enabled) {
+ std::shared_ptr<oboe::AudioStream> oboeStream = engine.getCurrentActivity()->getStream(streamIndex);
+ if (oboeStream != nullptr) {
+ oboeStream->setPerformanceHintEnabled(enabled);
+ }
+}
+
JNIEXPORT jint JNICALL
Java_com_mobileer_oboetester_OboeAudioStream_getBufferCapacityInFrames(
JNIEnv *env, jobject, jint streamIndex) {
@@ -332,6 +375,38 @@ Java_com_mobileer_oboetester_OboeAudioStream_getFormat(JNIEnv *env, jobject inst
}
JNIEXPORT jint JNICALL
+Java_com_mobileer_oboetester_OboeAudioStream_getHardwareChannelCount(
+ JNIEnv *env, jobject, jint streamIndex) {
+ jint result = (jint) oboe::Result::ErrorNull;
+ std::shared_ptr<oboe::AudioStream> oboeStream = engine.getCurrentActivity()->getStream(streamIndex);
+ if (oboeStream != nullptr) {
+ result = oboeStream->getHardwareChannelCount();
+ }
+ return result;
+}
+
+JNIEXPORT jint JNICALL
+Java_com_mobileer_oboetester_OboeAudioStream_getHardwareFormat(JNIEnv *env, jobject instance, jint streamIndex) {
+ jint result = (jint) oboe::Result::ErrorNull;
+ std::shared_ptr<oboe::AudioStream> oboeStream = engine.getCurrentActivity()->getStream(streamIndex);
+ if (oboeStream != nullptr) {
+ result = (jint) oboeStream->getHardwareFormat();
+ }
+ return result;
+}
+
+JNIEXPORT jint JNICALL
+Java_com_mobileer_oboetester_OboeAudioStream_getHardwareSampleRate(
+ JNIEnv *env, jobject, jint streamIndex) {
+ jint result = (jint) oboe::Result::ErrorNull;
+ std::shared_ptr<oboe::AudioStream> oboeStream = engine.getCurrentActivity()->getStream(streamIndex);
+ if (oboeStream != nullptr) {
+ result = oboeStream->getHardwareSampleRate();
+ }
+ return result;
+}
+
+JNIEXPORT jint JNICALL
Java_com_mobileer_oboetester_OboeAudioStream_getUsage(JNIEnv *env, jobject instance, jint streamIndex) {
jint result = (jint) oboe::Result::ErrorNull;
std::shared_ptr<oboe::AudioStream> oboeStream = engine.getCurrentActivity()->getStream(streamIndex);
@@ -434,11 +509,21 @@ Java_com_mobileer_oboetester_OboeAudioStream_getTimestampLatency(JNIEnv *env,
return engine.getCurrentActivity()->getTimestampLatency(streamIndex);
}
-JNIEXPORT jdouble JNICALL
+JNIEXPORT jfloat JNICALL
Java_com_mobileer_oboetester_OboeAudioStream_getCpuLoad(JNIEnv *env, jobject instance, jint streamIndex) {
return engine.getCurrentActivity()->getCpuLoad();
}
+JNIEXPORT jfloat JNICALL
+Java_com_mobileer_oboetester_OboeAudioStream_getAndResetMaxCpuLoad(JNIEnv *env, jobject instance, jint streamIndex) {
+ return engine.getCurrentActivity()->getAndResetMaxCpuLoad();
+}
+
+JNIEXPORT jint JNICALL
+Java_com_mobileer_oboetester_OboeAudioStream_getAndResetCpuMask(JNIEnv *env, jobject instance, jint streamIndex) {
+ return (jint) engine.getCurrentActivity()->getAndResetCpuMask();
+}
+
JNIEXPORT jstring JNICALL
Java_com_mobileer_oboetester_OboeAudioStream_getCallbackTimeString(JNIEnv *env, jobject instance) {
return env->NewStringUTF(engine.getCurrentActivity()->getCallbackTimeString().c_str());
@@ -446,16 +531,23 @@ Java_com_mobileer_oboetester_OboeAudioStream_getCallbackTimeString(JNIEnv *env,
JNIEXPORT void JNICALL
Java_com_mobileer_oboetester_OboeAudioStream_setWorkload(
- JNIEnv *env, jobject, jdouble workload) {
+ JNIEnv *env, jobject, jint workload) {
engine.getCurrentActivity()->setWorkload(workload);
}
+JNIEXPORT void JNICALL
+Java_com_mobileer_oboetester_OboeAudioStream_setHearWorkload(
+ JNIEnv *env, jobject, jint streamIndex, jboolean enabled) {
+ engine.getCurrentActivity()->setHearWorkload(enabled);
+}
+
JNIEXPORT jint JNICALL
Java_com_mobileer_oboetester_OboeAudioStream_getState(JNIEnv *env, jobject instance, jint 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) {
+ if (state != oboe::StreamState::Starting && state != oboe::StreamState::Started
+ && state != oboe::StreamState::Disconnected) {
oboe::Result result = oboeStream->waitForStateChange(
oboe::StreamState::Uninitialized,
&state, 0);
@@ -495,6 +587,12 @@ Java_com_mobileer_oboetester_OboeAudioStream_setCallbackReturnStop(JNIEnv *env,
}
JNIEXPORT void JNICALL
+Java_com_mobileer_oboetester_OboeAudioStream_setHangTimeMillis(JNIEnv *env, jclass type,
+ jint hangTimeMillis) {
+ OboeTesterStreamCallback::setHangTimeMillis(hangTimeMillis);
+}
+
+JNIEXPORT void JNICALL
Java_com_mobileer_oboetester_OboeAudioStream_setCallbackSize(JNIEnv *env, jclass type,
jint callbackSize) {
ActivityContext::callbackSize = callbackSize;
@@ -525,6 +623,11 @@ Java_com_mobileer_oboetester_OboeAudioOutputStream_setSignalType(
engine.getCurrentActivity()->setSignalType(signalType);
}
+JNIEXPORT void JNICALL
+Java_com_mobileer_oboetester_OboeAudioOutputStream_setAmplitude(JNIEnv *env, jobject, jfloat amplitude) {
+ engine.getCurrentActivity()->setAmplitude(amplitude);
+}
+
JNIEXPORT jint JNICALL
Java_com_mobileer_oboetester_OboeAudioStream_getOboeVersionNumber(JNIEnv *env,
jclass type) {
@@ -650,11 +753,23 @@ Java_com_mobileer_oboetester_AnalyzerActivity_getResetCount(JNIEnv *env,
// ==========================================================================
JNIEXPORT jint JNICALL
Java_com_mobileer_oboetester_GlitchActivity_getGlitchCount(JNIEnv *env,
- jobject instance) {
+ jobject instance) {
return engine.mActivityGlitches.getGlitchAnalyzer()->getGlitchCount();
}
JNIEXPORT jint JNICALL
+Java_com_mobileer_oboetester_GlitchActivity_getGlitchLength(JNIEnv *env,
+ jobject instance) {
+ return engine.mActivityGlitches.getGlitchAnalyzer()->getGlitchLength();
+}
+
+JNIEXPORT double JNICALL
+Java_com_mobileer_oboetester_GlitchActivity_getPhase(JNIEnv *env,
+ jobject instance) {
+ return engine.mActivityGlitches.getGlitchAnalyzer()->getPhaseOffset();
+}
+
+JNIEXPORT jint JNICALL
Java_com_mobileer_oboetester_GlitchActivity_getStateFrameCount(JNIEnv *env,
jobject instance,
jint state) {
@@ -679,6 +794,12 @@ Java_com_mobileer_oboetester_GlitchActivity_getSineAmplitude(JNIEnv *env,
return engine.mActivityGlitches.getGlitchAnalyzer()->getSineAmplitude();
}
+JNIEXPORT jint JNICALL
+Java_com_mobileer_oboetester_GlitchActivity_getSinePeriod(JNIEnv *env,
+ jobject instance) {
+ return engine.mActivityGlitches.getGlitchAnalyzer()->getSinePeriod();
+}
+
JNIEXPORT jdouble JNICALL
Java_com_mobileer_oboetester_TestDataPathsActivity_getMagnitude(JNIEnv *env,
jobject instance) {
@@ -691,9 +812,9 @@ Java_com_mobileer_oboetester_TestDataPathsActivity_getMaxMagnitude(JNIEnv *env,
return engine.mActivityDataPath.getDataPathAnalyzer()->getMaxMagnitude();
}
-JNIEXPORT jdouble JNICALL
-Java_com_mobileer_oboetester_TestDataPathsActivity_getPhase(JNIEnv *env,
- jobject instance) {
+JNIEXPORT double JNICALL
+Java_com_mobileer_oboetester_TestDataPathsActivity_getPhaseDataPaths(JNIEnv *env,
+ jobject instance) {
return engine.mActivityDataPath.getDataPathAnalyzer()->getPhaseOffset();
}
@@ -707,6 +828,15 @@ Java_com_mobileer_oboetester_GlitchActivity_setTolerance(JNIEnv *env,
}
JNIEXPORT void JNICALL
+Java_com_mobileer_oboetester_GlitchActivity_setForcedGlitchDuration(JNIEnv *env,
+ jobject instance,
+ jint frames) {
+ if (engine.mActivityGlitches.getGlitchAnalyzer()) {
+ engine.mActivityGlitches.getGlitchAnalyzer()->setForcedGlitchDuration(frames);
+ }
+}
+
+JNIEXPORT void JNICALL
Java_com_mobileer_oboetester_GlitchActivity_setInputChannelNative(JNIEnv *env,
jobject instance,
jint channel) {
@@ -745,6 +875,21 @@ Java_com_mobileer_oboetester_ManualGlitchActivity_getGlitch(JNIEnv *env, jobject
return numSamples;
}
+JNIEXPORT jint JNICALL
+Java_com_mobileer_oboetester_ManualGlitchActivity_getRecentSamples(JNIEnv *env, jobject instance,
+ jfloatArray waveform_) {
+ float *waveform = env->GetFloatArrayElements(waveform_, nullptr);
+ jsize length = env->GetArrayLength(waveform_);
+ jsize numSamples = 0;
+ auto *analyzer = engine.mActivityGlitches.getGlitchAnalyzer();
+ if (analyzer) {
+ numSamples = analyzer->getRecentSamples(waveform, length);
+ }
+
+ env->ReleaseFloatArrayElements(waveform_, waveform, 0);
+ return numSamples;
+}
+
JNIEXPORT void JNICALL
Java_com_mobileer_oboetester_TestAudioActivity_setDefaultAudioValues(JNIEnv *env, jclass clazz,
jint audio_manager_sample_rate,
@@ -753,18 +898,84 @@ Java_com_mobileer_oboetester_TestAudioActivity_setDefaultAudioValues(JNIEnv *env
oboe::DefaultStreamValues::FramesPerBurst = audio_manager_frames_per_burst;
}
-static TestErrorCallback sTester;
+static TestErrorCallback sErrorCallbackTester;
JNIEXPORT void JNICALL
Java_com_mobileer_oboetester_TestErrorCallbackActivity_testDeleteCrash(
JNIEnv *env, jobject instance) {
- sTester.test();
+ sErrorCallbackTester.test();
}
JNIEXPORT jint JNICALL
Java_com_mobileer_oboetester_TestErrorCallbackActivity_getCallbackMagic(
JNIEnv *env, jobject instance) {
- return sTester.getCallbackMagic();
+ return sErrorCallbackTester.getCallbackMagic();
+}
+
+static TestRoutingCrash sRoutingCrash;
+
+JNIEXPORT jint JNICALL
+Java_com_mobileer_oboetester_TestRouteDuringCallbackActivity_startStream(
+ JNIEnv *env, jobject instance,
+ jboolean useInput) {
+ return sRoutingCrash.start(useInput);
+}
+
+JNIEXPORT jint JNICALL
+Java_com_mobileer_oboetester_TestRouteDuringCallbackActivity_stopStream(
+ JNIEnv *env, jobject instance) {
+ return sRoutingCrash.stop();
+}
+
+JNIEXPORT jint JNICALL
+Java_com_mobileer_oboetester_TestRouteDuringCallbackActivity_getSleepTimeMicros(
+ JNIEnv *env, jobject instance) {
+ return sRoutingCrash.getSleepTimeMicros();
+}
+
+static TestColdStartLatency sColdStartLatency;
+
+JNIEXPORT jint JNICALL
+Java_com_mobileer_oboetester_TestColdStartLatencyActivity_openStream(
+ JNIEnv *env, jobject instance,
+ jboolean useInput, jboolean useLowLatency, jboolean useMmap, jboolean useExclusive) {
+ return sColdStartLatency.open(useInput, useLowLatency, useMmap, useExclusive);
+}
+
+JNIEXPORT jint JNICALL
+Java_com_mobileer_oboetester_TestColdStartLatencyActivity_startStream(
+ JNIEnv *env, jobject instance) {
+ return sColdStartLatency.start();
+}
+
+JNIEXPORT jint JNICALL
+Java_com_mobileer_oboetester_TestColdStartLatencyActivity_closeStream(
+ JNIEnv *env, jobject instance) {
+ return sColdStartLatency.close();
+}
+
+JNIEXPORT jint JNICALL
+Java_com_mobileer_oboetester_TestColdStartLatencyActivity_getOpenTimeMicros(
+ JNIEnv *env, jobject instance) {
+ return sColdStartLatency.getOpenTimeMicros();
+}
+
+JNIEXPORT jint JNICALL
+Java_com_mobileer_oboetester_TestColdStartLatencyActivity_getStartTimeMicros(
+ JNIEnv *env, jobject instance) {
+ return sColdStartLatency.getStartTimeMicros();
+}
+
+JNIEXPORT jint JNICALL
+Java_com_mobileer_oboetester_TestColdStartLatencyActivity_getColdStartTimeMicros(
+ JNIEnv *env, jobject instance) {
+ return sColdStartLatency.getColdStartTimeMicros();
+}
+
+JNIEXPORT jint JNICALL
+Java_com_mobileer_oboetester_TestColdStartLatencyActivity_getAudioDeviceId(
+ JNIEnv *env, jobject instance) {
+ return sColdStartLatency.getDeviceId();
}
}
diff --git a/apps/OboeTester/app/src/main/cpp/synth/BiquadFilter.h b/apps/OboeTester/app/src/main/cpp/synth/BiquadFilter.h
new file mode 100644
index 00000000..20ee32e6
--- /dev/null
+++ b/apps/OboeTester/app/src/main/cpp/synth/BiquadFilter.h
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2016 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.
+ *
+ * This code was translated from the JSyn Java code.
+ * JSyn is Copyright 2009 Phil Burk, Mobileer Inc
+ * JSyn is licensed under the Apache License, Version 2.0
+ */
+
+#ifndef SYNTHMARK_BIQUAD_FILTER_H
+#define SYNTHMARK_BIQUAD_FILTER_H
+
+#include <cstdint>
+#include <math.h>
+#include "SynthTools.h"
+#include "UnitGenerator.h"
+
+namespace marksynth {
+
+#define BIQUAD_MIN_FREQ (0.00001f) // REVIEW
+#define BIQUAD_MIN_Q (0.00001f) // REVIEW
+
+#define RECALCULATE_PER_SAMPLE 0
+
+/**
+ * Time varying lowpass resonant filter.
+ */
+class BiquadFilter : public UnitGenerator
+{
+public:
+ BiquadFilter()
+ : mQ(1.0)
+ {
+ xn1 = xn2 = yn1 = yn2 = (synth_float_t) 0;
+ a0 = a1 = a2 = b1 = b2 = (synth_float_t) 0;
+ }
+
+ virtual ~BiquadFilter() = default;
+
+ /**
+ * Resonance, typically between 1.0 and 10.0.
+ * Input will clipped at a BIQUAD_MIN_Q.
+ */
+ void setQ(synth_float_t q) {
+ if( q < BIQUAD_MIN_Q ) {
+ q = BIQUAD_MIN_Q;
+ }
+ mQ = q;
+ }
+
+ synth_float_t getQ() {
+ return mQ;
+ }
+
+ void generate(synth_float_t *input,
+ synth_float_t *frequencies,
+ int32_t numSamples) {
+ synth_float_t xn, yn;
+
+#if RECALCULATE_PER_SAMPLE == 0
+ calculateCoefficients(frequencies[0], mQ);
+#endif
+ for (int i = 0; i < numSamples; i++) {
+#if RECALCULATE_PER_SAMPLE == 1
+ calculateCoefficients(frequencies[i], mQ);
+#endif
+ // Generate outputs by filtering inputs.
+ xn = input[i];
+ synth_float_t finite = (a0 * xn) + (a1 * xn1) + (a2 * xn2);
+ // Use double precision for recursive portion.
+ yn = finite - (b1 * yn1) - (b2 * yn2);
+ output[i] = (synth_float_t) yn;
+
+ // Delay input and output values.
+ xn2 = xn1;
+ xn1 = xn;
+ yn2 = yn1;
+ yn1 = yn;
+ }
+
+ // Apply a small bipolar impulse to filter to prevent arithmetic underflow.
+ yn1 += (synth_float_t) 1.0E-26;
+ yn2 -= (synth_float_t) 1.0E-26;
+ }
+
+
+private:
+ synth_float_t mQ;
+
+ synth_float_t xn1; // delay lines
+ synth_float_t xn2;
+ double yn1;
+ double yn2;
+
+ synth_float_t a0; // coefficients
+ synth_float_t a1;
+ synth_float_t a2;
+
+ synth_float_t b1;
+ synth_float_t b2;
+
+ synth_float_t cos_omega;
+ synth_float_t sin_omega;
+ synth_float_t alpha;
+
+ // Calculate coefficients common to many parametric biquad filters.
+ void calcCommon( synth_float_t ratio, synth_float_t Q )
+ {
+ synth_float_t omega;
+
+ /* Don't let frequency get too close to Nyquist or filter will blow up. */
+ if( ratio >= 0.499f ) ratio = 0.499f;
+ omega = 2.0f * (synth_float_t)M_PI * ratio;
+
+#if 1
+ // This is not significantly faster on Mac or Linux.
+ cos_omega = SynthTools::fastCosine(omega);
+ sin_omega = SynthTools::fastSine(omega );
+#else
+ {
+ float fsin_omega;
+ float fcos_omega;
+ sincosf(omega, &fsin_omega, &fcos_omega);
+ cos_omega = (synth_float_t) fcos_omega;
+ sin_omega = (synth_float_t) fsin_omega;
+ }
+#endif
+ alpha = sin_omega / (2.0f * Q);
+ }
+
+ // Lowpass coefficients
+ void calculateCoefficients( synth_float_t frequency, synth_float_t Q )
+ {
+ synth_float_t scalar, omc;
+
+ if( frequency < BIQUAD_MIN_FREQ ) frequency = BIQUAD_MIN_FREQ;
+
+ calcCommon( frequency * mSamplePeriod, Q );
+
+ scalar = 1.0f / (1.0f + alpha);
+ omc = (1.0f - cos_omega);
+
+ a0 = omc * 0.5f * scalar;
+ a1 = omc * scalar;
+ a2 = a0;
+ b1 = -2.0f * cos_omega * scalar;
+ b2 = (1.0f - alpha) * scalar;
+ }
+};
+
+};
+#endif // SYNTHMARK_BIQUAD_FILTER_H
diff --git a/apps/OboeTester/app/src/main/cpp/synth/DifferentiatedParabola.h b/apps/OboeTester/app/src/main/cpp/synth/DifferentiatedParabola.h
new file mode 100644
index 00000000..c236147e
--- /dev/null
+++ b/apps/OboeTester/app/src/main/cpp/synth/DifferentiatedParabola.h
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2016 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.
+ *
+ * This code was translated from the JSyn Java code.
+ * JSyn is Copyright 2009 Phil Burk, Mobileer Inc
+ * JSyn is licensed under the Apache License, Version 2.0
+ */
+
+#ifndef SYNTHMARK_DIFFERENTIATED_PARABOLA_H
+#define SYNTHMARK_DIFFERENTIATED_PARABOLA_H
+
+#include <cstdint>
+#include <math.h>
+#include "SynthTools.h"
+
+namespace marksynth {
+
+constexpr double kDPWVeryLowFrequency = 2.0 * 0.1 / kSynthmarkSampleRate;
+
+/**
+ * DPW is a tool for generating band-limited waveforms
+ * based on a paper by Antti Huovilainen and Vesa Valimaki:
+ * "New Approaches to Digital Subtractive Synthesis"
+ */
+class DifferentiatedParabola
+{
+public:
+ DifferentiatedParabola()
+ : mZ1(0)
+ , mZ2(0) {}
+
+ virtual ~DifferentiatedParabola() = default;
+
+ synth_float_t next(synth_float_t phase, synth_float_t phaseIncrement) {
+ synth_float_t dpw;
+ synth_float_t positivePhaseIncrement = (phaseIncrement < 0.0)
+ ? phaseIncrement
+ : 0.0 - phaseIncrement;
+
+ // If the frequency is very low then just use the raw sawtooth.
+ // This avoids divide by zero problems and scaling problems.
+ if (positivePhaseIncrement < kDPWVeryLowFrequency) {
+ dpw = phase;
+ } else {
+ // Calculate the parabola.
+ synth_float_t squared = phase * phase;
+ // Differentiate using a delayed value.
+ synth_float_t diffed = squared - mZ2;
+ // Delay line.
+ // TODO - Why Z2. Vesa's paper says use Z1?
+ mZ2 = mZ1;
+ mZ1 = squared;
+
+ // Calculate scaling
+ dpw = diffed * 0.25f / positivePhaseIncrement; // TODO extract and optimize
+ }
+ return dpw;
+ }
+
+private:
+ synth_float_t mZ1;
+ synth_float_t mZ2;
+};
+
+};
+#endif // SYNTHMARK_DIFFERENTIATED_PARABOLA_H
diff --git a/apps/OboeTester/app/src/main/cpp/synth/EnvelopeADSR.h b/apps/OboeTester/app/src/main/cpp/synth/EnvelopeADSR.h
new file mode 100644
index 00000000..83fab9b6
--- /dev/null
+++ b/apps/OboeTester/app/src/main/cpp/synth/EnvelopeADSR.h
@@ -0,0 +1,229 @@
+/*
+ * Copyright (C) 2016 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.
+ *
+ * This code was translated from the JSyn Java code.
+ * JSyn is Copyright 2009 Phil Burk, Mobileer Inc
+ * JSyn is licensed under the Apache License, Version 2.0
+ */
+
+#ifndef SYNTHMARK_ENVELOPE_ADSR_H
+#define SYNTHMARK_ENVELOPE_ADSR_H
+
+#include <cstdint>
+#include <math.h>
+#include "SynthTools.h"
+#include "UnitGenerator.h"
+
+namespace marksynth {
+
+/**
+ * Generate a contour that can be used to control amplitude or
+ * other parameters.
+ */
+
+class EnvelopeADSR : public UnitGenerator
+{
+public:
+ EnvelopeADSR()
+ : mAttack(0.05)
+ , mDecay(0.6)
+ , mSustainLevel(0.4)
+ , mRelease(2.5)
+ {}
+
+ virtual ~EnvelopeADSR() = default;
+
+#define MIN_DURATION (1.0 / 100000.0)
+
+ enum State {
+ IDLE, ATTACKING, DECAYING, SUSTAINING, RELEASING
+ };
+
+ void setGate(bool gate) {
+ triggered = gate;
+ }
+
+ bool isIdle() {
+ return mState == State::IDLE;
+ }
+
+ /**
+ * Time in seconds for the falling stage to go from 0 dB to -90 dB. The decay stage will stop at
+ * the sustain level. But we calculate the time to fall to -90 dB so that the decay
+ * <em>rate</em> will be unaffected by the sustain level.
+ */
+ void setDecayTime(synth_float_t time) {
+ mDecay = time;
+ }
+
+ synth_float_t getDecayTime() {
+ return mDecay;
+ }
+
+ /**
+ * Time in seconds for the rising stage of the envelope to go from 0.0 to 1.0. The attack is a
+ * linear ramp.
+ */
+ void setAttackTime(synth_float_t time) {
+ mAttack = time;
+ }
+
+ synth_float_t getAttackTime() {
+ return mAttack;
+ }
+
+ void generate(int32_t numSamples) {
+ for (int i = 0; i < numSamples; i++) {
+ switch (mState) {
+ case IDLE:
+ for (; i < numSamples; i++) {
+ output[i] = mLevel;
+ if (triggered) {
+ startAttack();
+ break;
+ }
+ }
+ break;
+
+ case ATTACKING:
+ for (; i < numSamples; i++) {
+ // Increment first so we can render fast attacks.
+ mLevel += increment;
+ if (mLevel >= 1.0) {
+ mLevel = 1.0;
+ output[i] = mLevel;
+ startDecay();
+ break;
+ } else {
+ output[i] = mLevel;
+ if (!triggered) {
+ startRelease();
+ break;
+ }
+ }
+ }
+ break;
+
+ case DECAYING:
+ for (; i < numSamples; i++) {
+ output[i] = mLevel;
+ mLevel *= mScaler; // exponential decay
+ if (mLevel < kAmplitudeDb96) {
+ startIdle();
+ break;
+ } else if (!triggered) {
+ startRelease();
+ break;
+ } else if (mLevel < mSustainLevel) {
+ mLevel = mSustainLevel;
+ startSustain();
+ break;
+ }
+ }
+ break;
+
+ case SUSTAINING:
+ for (; i < numSamples; i++) {
+ mLevel = mSustainLevel;
+ output[i] = mLevel;
+ if (!triggered) {
+ startRelease();
+ break;
+ }
+ }
+ break;
+
+ case RELEASING:
+ for (; i < numSamples; i++) {
+ output[i] = mLevel;
+ mLevel *= mScaler; // exponential decay
+ if (triggered) {
+ startAttack();
+ break;
+ } else if (mLevel < kAmplitudeDb96) {
+ startIdle();
+ break;
+ }
+ }
+ break;
+ }
+ }
+ }
+
+private:
+
+ void startIdle() {
+ mState = State::IDLE;
+ mLevel = 0.0;
+ }
+
+ void startAttack() {
+ if (mAttack < MIN_DURATION) {
+ mLevel = 1.0;
+ startDecay();
+ } else {
+ increment = mSamplePeriod / mAttack;
+ mState = State::ATTACKING;
+ }
+ }
+
+ void startDecay() {
+ double duration = mDecay;
+ if (duration < MIN_DURATION) {
+ startSustain();
+ } else {
+ mScaler = SynthTools::convertTimeToExponentialScaler(duration, mSampleRate);
+ mState = State::DECAYING;
+ }
+ }
+
+ void startSustain() {
+ mState = State::SUSTAINING;
+ }
+
+ void startRelease() {
+ double duration = mRelease;
+ if (duration < MIN_DURATION) {
+ duration = MIN_DURATION;
+ }
+ mScaler = SynthTools::convertTimeToExponentialScaler(duration, mSampleRate);
+ mState = State::RELEASING;
+ }
+
+ synth_float_t mAttack;
+ synth_float_t mDecay;
+ /**
+ * Level for the sustain stage. The envelope will hold here until the input goes to zero or
+ * less. This should be set between 0.0 and 1.0.
+ */
+ synth_float_t mSustainLevel;
+ /**
+ * Time in seconds to go from 0 dB to -90 dB. This stage is triggered when the input goes to
+ * zero or less. The release stage will start from the sustain level. But we calculate the time
+ * to fall from full amplitude so that the release <em>rate</em> will be unaffected by the
+ * sustain level.
+ */
+ synth_float_t mRelease;
+
+ State mState = State::IDLE;
+ synth_float_t mScaler = 1.0;
+ synth_float_t mLevel = 0.0;
+ synth_float_t increment = 0;
+ bool triggered = false;
+
+};
+
+};
+#endif // SYNTHMARK_ENVELOPE_ADSR_H
diff --git a/apps/OboeTester/app/src/main/cpp/synth/IncludeMeOnce.h b/apps/OboeTester/app/src/main/cpp/synth/IncludeMeOnce.h
new file mode 100644
index 00000000..bf6f1b0e
--- /dev/null
+++ b/apps/OboeTester/app/src/main/cpp/synth/IncludeMeOnce.h
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2016 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.
+ *
+ * This code was translated from the JSyn Java code.
+ * JSyn is Copyright 2009 Phil Burk, Mobileer Inc
+ * JSyn is licensed under the Apache License, Version 2.0
+ */
+
+#ifndef INCLUDE_ME_ONCE_H
+#define INCLUDE_ME_ONCE_H
+
+#include "UnitGenerator.h"
+#include "PitchToFrequency.h"
+
+namespace marksynth {
+
+//synth statics
+int32_t UnitGenerator::mSampleRate = kSynthmarkSampleRate;
+synth_float_t UnitGenerator::mSamplePeriod = 1.0f / kSynthmarkSampleRate;
+
+PowerOfTwoTable PitchToFrequency::mPowerTable(64);
+};
+
+#endif //INCLUDE_ME_ONCE_H
diff --git a/apps/OboeTester/app/src/main/cpp/synth/LookupTable.h b/apps/OboeTester/app/src/main/cpp/synth/LookupTable.h
new file mode 100644
index 00000000..07423637
--- /dev/null
+++ b/apps/OboeTester/app/src/main/cpp/synth/LookupTable.h
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2016 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.
+ *
+ * This code was translated from the JSyn Java code.
+ * JSyn is Copyright 2009 Phil Burk, Mobileer Inc
+ * JSyn is licensed under the Apache License, Version 2.0
+ */
+
+#ifndef SYNTHMARK_LOOKUP_TABLE_H
+#define SYNTHMARK_LOOKUP_TABLE_H
+
+#include <cstdint>
+#include "SynthTools.h"
+
+namespace marksynth {
+
+class LookupTable {
+public:
+ LookupTable(int32_t numEntries)
+ : mNumEntries(numEntries)
+ {}
+
+ virtual ~LookupTable() {
+ delete[] mTable;
+ }
+
+ void fillTable() {
+ // Add 2 guard points for interpolation and roundoff error.
+ int tableSize = mNumEntries + 2;
+ mTable = new float[tableSize];
+ // Fill the table with calculated values
+ float scale = 1.0f / mNumEntries;
+ for (int i = 0; i < tableSize; i++) {
+ float value = calculate(i * scale);
+ mTable[i] = value;
+ }
+ }
+
+ /**
+ * @param input normalized between 0.0 and 1.0
+ */
+ float lookup(float input) {
+ float fractionalTableIndex = input * mNumEntries;
+ int32_t index = (int) floor(fractionalTableIndex);
+ float fraction = fractionalTableIndex - index;
+ float baseValue = mTable[index];
+ float value = baseValue
+ + (fraction * (mTable[index + 1] - baseValue));
+ return value;
+ }
+
+ virtual float calculate(float input) = 0;
+
+private:
+ int32_t mNumEntries;
+ synth_float_t *mTable;
+};
+
+};
+#endif // SYNTHMARK_LOOKUP_TABLE_H
diff --git a/apps/OboeTester/app/src/main/cpp/synth/PitchToFrequency.h b/apps/OboeTester/app/src/main/cpp/synth/PitchToFrequency.h
new file mode 100644
index 00000000..a06d3276
--- /dev/null
+++ b/apps/OboeTester/app/src/main/cpp/synth/PitchToFrequency.h
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2016 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.
+ *
+ * This code was translated from the JSyn Java code.
+ * JSyn is Copyright 2009 Phil Burk, Mobileer Inc
+ * JSyn is licensed under the Apache License, Version 2.0
+ */
+
+#ifndef SYNTHMARK_PITCH_TO_FREQUENCY_H
+#define SYNTHMARK_PITCH_TO_FREQUENCY_H
+
+#include <cstdint>
+#include <math.h>
+#include "SynthTools.h"
+#include "LookupTable.h"
+
+namespace marksynth {
+
+constexpr int kSemitonesPerOctave = 12;
+// Pitches are in semitones based on the MIDI standard.
+constexpr int kPitchMiddleC = 60;
+constexpr double kFrequencyMiddleC = 261.625549;
+
+class PowerOfTwoTable : public LookupTable {
+public:
+ PowerOfTwoTable(int32_t numEntries)
+ : LookupTable(numEntries)
+ {
+ fillTable();
+ }
+
+ virtual ~PowerOfTwoTable() {}
+
+ virtual float calculate(float input) override {
+ return powf(2.0f, input);
+ }
+};
+
+class PitchToFrequency
+{
+public:
+ PitchToFrequency() {}
+
+ virtual ~PitchToFrequency() {
+ }
+
+ static double convertPitchToFrequency(double pitch) {
+ double exponent = (pitch - kPitchMiddleC) * (1.0 / kSemitonesPerOctave);
+ return kFrequencyMiddleC * pow(2.0, exponent);
+ }
+
+ synth_float_t lookupPitchToFrequency(synth_float_t pitch) {
+ // Only calculate if input changed since last time.
+ if (pitch != lastInput) {
+ synth_float_t octavePitch = (pitch - kPitchMiddleC) * (1.0 / kSemitonesPerOctave);
+ int32_t octaveIndex = (int) floor(octavePitch);
+ synth_float_t fractionalOctave = octavePitch - octaveIndex;
+
+ // Do table lookup.
+ synth_float_t value = kFrequencyMiddleC * mPowerTable.lookup(fractionalOctave);
+
+ // Adjust for octave by multiplying by a power of 2. Allow for +/- 16 octaves;
+ const int32_t octaveOffset = 16;
+ synth_float_t octaveScaler = ((synth_float_t)(1 << (octaveIndex + octaveOffset)))
+ * (1.0 / (1 << octaveOffset));
+ value *= octaveScaler;
+
+ lastInput = pitch;
+ lastOutput = value;
+ }
+ return lastOutput;
+ }
+
+ /**
+ * @param pitches an array of fractional MIDI pitches
+ */
+ void generate(const synth_float_t *pitches, synth_float_t *frequencies, int32_t count) {
+ for (int i = 0; i < count; i++) {
+ frequencies[i] = lookupPitchToFrequency(pitches[i]);
+ }
+ }
+
+private:
+ static PowerOfTwoTable mPowerTable;
+
+ synth_float_t lastInput = kPitchMiddleC;
+ synth_float_t lastOutput = kFrequencyMiddleC;
+
+};
+
+};
+#endif // SYNTHMARK_PITCH_TO_FREQUENCY_H
diff --git a/apps/OboeTester/app/src/main/cpp/synth/SawtoothOscillator.h b/apps/OboeTester/app/src/main/cpp/synth/SawtoothOscillator.h
new file mode 100644
index 00000000..f27fafaa
--- /dev/null
+++ b/apps/OboeTester/app/src/main/cpp/synth/SawtoothOscillator.h
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2016 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.
+ *
+ * This code was translated from the JSyn Java code.
+ * JSyn is Copyright 2009 Phil Burk, Mobileer Inc
+ * JSyn is licensed under the Apache License, Version 2.0
+ */
+
+#ifndef SYNTHMARK_SAWTOOTH_OSCILLATOR_H
+#define SYNTHMARK_SAWTOOTH_OSCILLATOR_H
+
+#include <cstdint>
+#include <math.h>
+#include "SynthTools.h"
+#include "UnitGenerator.h"
+#include "DifferentiatedParabola.h"
+
+namespace marksynth {
+/**
+ * Simple phasor that can be used to implement other oscillators.
+ * Note that this is NON-bandlimited and should not be used
+ * directly as a sound source.
+ */
+class SawtoothOscillator : public UnitGenerator
+{
+public:
+ SawtoothOscillator()
+ : mPhase(0) {}
+
+ virtual ~SawtoothOscillator() = default;
+
+ void generate(synth_float_t frequency, int32_t numSamples) {
+ synth_float_t phase = mPhase;
+ synth_float_t phaseIncrement = 2.0 * frequency * mSamplePeriod;
+ for (int i = 0; i < numSamples; i++) {
+ output[i] = translatePhase(phase, phaseIncrement);
+ phase += phaseIncrement;
+ if (phase > 1.0) {
+ phase -= 2.0;
+ }
+ }
+ mPhase = phase;
+ }
+
+ void generate(synth_float_t *frequencies, int32_t numSamples) {
+ synth_float_t phase = mPhase;
+ for (int i = 0; i < numSamples; i++) {
+ synth_float_t phaseIncrement = 2.0 * frequencies[i] * mSamplePeriod;
+ output[i] = translatePhase(phase, phaseIncrement);
+ phase += phaseIncrement;
+ if (phase > 1.0) {
+ phase -= 2.0;
+ }
+ }
+ mPhase = phase;
+ }
+
+ virtual synth_float_t translatePhase(synth_float_t phase, synth_float_t phaseIncrement) {
+ (void) phaseIncrement;
+ return phase;
+ }
+
+private:
+ synth_float_t mPhase; // between -1.0 and +1.0
+};
+
+};
+#endif // SYNTHMARK_SAWTOOTH_OSCILLATOR_H
diff --git a/apps/OboeTester/app/src/main/cpp/synth/SawtoothOscillatorDPW.h b/apps/OboeTester/app/src/main/cpp/synth/SawtoothOscillatorDPW.h
new file mode 100644
index 00000000..a6eb4015
--- /dev/null
+++ b/apps/OboeTester/app/src/main/cpp/synth/SawtoothOscillatorDPW.h
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2016 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.
+ *
+ * This code was translated from the JSyn Java code.
+ * JSyn is Copyright 2009 Phil Burk, Mobileer Inc
+ * JSyn is licensed under the Apache License, Version 2.0
+ */
+
+#ifndef SYNTHMARK_SAWTOOTH_OSCILLATOR_DPW_H
+#define SYNTHMARK_SAWTOOTH_OSCILLATOR_DPW_H
+
+#include <cstdint>
+#include <math.h>
+#include "SynthTools.h"
+#include "DifferentiatedParabola.h"
+
+namespace marksynth {
+/**
+ * Band limited sawtooth oscillator.
+ * Suitable as a sound source.
+ */
+
+class SawtoothOscillatorDPW : public SawtoothOscillator
+{
+public:
+ SawtoothOscillatorDPW()
+ : SawtoothOscillator()
+ , dpw() {}
+
+ virtual ~SawtoothOscillatorDPW() = default;
+
+ virtual inline synth_float_t translatePhase(synth_float_t phase, synth_float_t phaseIncrement) {
+ return dpw.next(phase, phaseIncrement);
+ }
+
+private:
+ DifferentiatedParabola dpw;
+};
+
+};
+#endif // SYNTHMARK_SAWTOOTH_OSCILLATOR_DPW_H
diff --git a/apps/OboeTester/app/src/main/cpp/synth/SimpleVoice.h b/apps/OboeTester/app/src/main/cpp/synth/SimpleVoice.h
new file mode 100644
index 00000000..68e00038
--- /dev/null
+++ b/apps/OboeTester/app/src/main/cpp/synth/SimpleVoice.h
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2016 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 SYNTHMARK_SIMPLE_VOICE_H
+#define SYNTHMARK_SIMPLE_VOICE_H
+
+#include <cstdint>
+#include <math.h>
+#include "SynthTools.h"
+#include "VoiceBase.h"
+#include "SawtoothOscillator.h"
+#include "SawtoothOscillatorDPW.h"
+#include "SquareOscillatorDPW.h"
+#include "SineOscillator.h"
+#include "EnvelopeADSR.h"
+#include "PitchToFrequency.h"
+#include "BiquadFilter.h"
+
+namespace marksynth {
+/**
+ * Classic subtractive synthesizer voice with
+ * 2 LFOs, 2 audio oscillators, filter and envelopes.
+ */
+class SimpleVoice : public VoiceBase
+{
+public:
+ SimpleVoice()
+ : VoiceBase()
+ , mLfo1()
+ , mOsc1()
+ , mOsc2()
+ , mPitchToFrequency()
+ , mFilter()
+ , mFilterEnvelope()
+ , mAmplitudeEnvelope()
+ // The following values are arbitrary but typical values.
+ , mDetune(1.0001f) // slight phasing
+ , mVibratoDepth(0.03f)
+ , mVibratoRate(6.0f)
+ , mFilterEnvDepth(3000.0f)
+ , mFilterCutoff(400.0f)
+ {
+ mFilter.setQ(2.0);
+ // Randomize attack times to smooth out CPU load for envelope state transitions.
+ mFilterEnvelope.setAttackTime(0.05 + (0.2 * SynthTools::nextRandomDouble()));
+ mFilterEnvelope.setDecayTime(7.0 + (1.0 * SynthTools::nextRandomDouble()));
+ mAmplitudeEnvelope.setAttackTime(0.02 + (0.05 * SynthTools::nextRandomDouble()));
+ mAmplitudeEnvelope.setDecayTime(1.0 + (0.2 * SynthTools::nextRandomDouble()));
+ }
+
+ virtual ~SimpleVoice() = default;
+
+ void setPitch(synth_float_t pitch) {
+ mPitch = pitch;
+ }
+
+ void noteOn(synth_float_t pitch, synth_float_t velocity) {
+ (void) velocity; // TODO use velocity?
+ mPitch = pitch;
+ mFilterEnvelope.setGate(true);
+ mAmplitudeEnvelope.setGate(true);
+ }
+
+ void noteOff() {
+ mFilterEnvelope.setGate(false);
+ mAmplitudeEnvelope.setGate(false);
+ }
+
+ void generate(int32_t numFrames) {
+ assert(numFrames <= kSynthmarkFramesPerRender);
+
+ // LFO #1 - vibrato
+ mLfo1.generate(mVibratoRate, numFrames);
+ synth_float_t *pitches = mBuffer1;
+ SynthTools::scaleOffsetBuffer(mLfo1.output, pitches, numFrames, mVibratoDepth, mPitch);
+ synth_float_t *frequencies = mBuffer2;
+ mPitchToFrequency.generate(pitches, frequencies, numFrames);
+
+ // OSC #1 - sawtooth
+ mOsc1.generate(frequencies, numFrames);
+
+ // OSC #2 - detuned square wave oscillator
+ SynthTools::scaleBuffer(frequencies, frequencies, numFrames, mDetune);
+ mOsc2.generate(frequencies, numFrames);
+
+ // Mix the two oscillators
+ synth_float_t *mixed = frequencies;
+ SynthTools::mixBuffers(mOsc1.output, 0.6, mOsc2.output, 0.4, mixed, numFrames);
+
+ // Filter envelope
+ mFilterEnvelope.generate(numFrames);
+ synth_float_t *cutoffFrequencies = pitches; // reuse unneeded buffer
+ SynthTools::scaleOffsetBuffer(mFilterEnvelope.output, cutoffFrequencies, numFrames,
+ mFilterEnvDepth, mFilterCutoff);
+
+ // Biquad resonant low-pass filter
+ mFilter.generate(mixed, cutoffFrequencies, numFrames);
+
+ // Amplitude ADSR
+ mAmplitudeEnvelope.generate(numFrames);
+ SynthTools::multiplyBuffers(mFilter.output, mAmplitudeEnvelope.output, output, numFrames);
+ }
+
+private:
+ SineOscillator mLfo1;
+ SawtoothOscillatorDPW mOsc1;
+ SquareOscillatorDPW mOsc2;
+ PitchToFrequency mPitchToFrequency;
+ BiquadFilter mFilter;
+ EnvelopeADSR mFilterEnvelope;
+ EnvelopeADSR mAmplitudeEnvelope;
+
+ synth_float_t mDetune; // frequency scaler
+ synth_float_t mVibratoDepth; // in semitones
+ synth_float_t mVibratoRate; // in Hertz
+ synth_float_t mFilterEnvDepth; // in Hertz
+ synth_float_t mFilterCutoff; // in Hertz
+
+ // Buffers for storing signals that are being passed between units.
+ synth_float_t mBuffer1[kSynthmarkFramesPerRender];
+ synth_float_t mBuffer2[kSynthmarkFramesPerRender];
+};
+};
+#endif // SYNTHMARK_SIMPLE_VOICE_H
diff --git a/apps/OboeTester/app/src/main/cpp/synth/SineOscillator.h b/apps/OboeTester/app/src/main/cpp/synth/SineOscillator.h
new file mode 100644
index 00000000..6a1c55d6
--- /dev/null
+++ b/apps/OboeTester/app/src/main/cpp/synth/SineOscillator.h
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2016 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.
+ *
+ * This code was translated from the JSyn Java code.
+ * JSyn is Copyright 2009 Phil Burk, Mobileer Inc
+ * JSyn is licensed under the Apache License, Version 2.0
+ */
+
+#ifndef SYNTHMARK_SINE_OSCILLATOR_H
+#define SYNTHMARK_SINE_OSCILLATOR_H
+
+#include <cstdint>
+#include <math.h>
+#include "SynthTools.h"
+
+namespace marksynth {
+class SineOscillator : public SawtoothOscillator
+{
+public:
+ SineOscillator()
+ : SawtoothOscillator() {}
+
+ virtual ~SineOscillator() = default;
+
+ virtual inline synth_float_t translatePhase(synth_float_t phase, synth_float_t phaseIncrement) {
+ (void) phaseIncrement;
+ return SynthTools::fastSine(phase * M_PI);
+ }
+
+};
+};
+#endif // SYNTHMARK_SINE_OSCILLATOR_H
diff --git a/apps/OboeTester/app/src/main/cpp/synth/SquareOscillatorDPW.h b/apps/OboeTester/app/src/main/cpp/synth/SquareOscillatorDPW.h
new file mode 100644
index 00000000..01587152
--- /dev/null
+++ b/apps/OboeTester/app/src/main/cpp/synth/SquareOscillatorDPW.h
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2016 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.
+ *
+ * This code was translated from the JSyn Java code.
+ * JSyn is Copyright 2009 Phil Burk, Mobileer Inc
+ * JSyn is licensed under the Apache License, Version 2.0
+ */
+
+#ifndef SYNTHMARK_SQUARE_OSCILLATOR_DPW_H
+#define SYNTHMARK_SQUARE_OSCILLATOR_DPW_H
+
+#include <cstdint>
+#include <math.h>
+#include "SynthTools.h"
+#include "DifferentiatedParabola.h"
+
+namespace marksynth {
+/**
+ * Square waves contains the odd partials of a fundamental.
+ * The square wave is generated by combining two sawtooth waveforms
+ * that are 180 degrees out of phase. This causes the even partials
+ * to be cancelled out.
+ */
+class SquareOscillatorDPW : public SawtoothOscillator
+{
+public:
+ SquareOscillatorDPW()
+ : SawtoothOscillator()
+ , dpw1()
+ , dpw2() {}
+
+ virtual ~SquareOscillatorDPW() = default;
+
+ virtual inline synth_float_t translatePhase(synth_float_t phase1,
+ synth_float_t phaseIncrement) {
+ synth_float_t val1 = dpw1.next(phase1, phaseIncrement);
+
+ /* Generate second sawtooth so we can add them together. */
+ synth_float_t phase2 = phase1 + 1.0; /* 180 degrees out of phase. */
+ if (phase2 >= 1.0)
+ phase2 -= 2.0;
+ synth_float_t val2 = dpw1.next(phase2, phaseIncrement);
+
+ /*
+ * Need to adjust amplitude based on positive phaseInc. little less than half at
+ * Nyquist/2.0!
+ */
+ const synth_float_t STARTAMP = 0.92; // derived empirically
+ synth_float_t positivePhaseIncrement = (phaseIncrement < 0.0)
+ ? phaseIncrement
+ : 0.0 - phaseIncrement;
+ synth_float_t scale = STARTAMP - positivePhaseIncrement;
+ return scale * (val1 - val2);
+ }
+
+private:
+ DifferentiatedParabola dpw1;
+ DifferentiatedParabola dpw2;
+};
+};
+#endif // SYNTHMARK_SQUARE_OSCILLATOR_DPW_H
diff --git a/apps/OboeTester/app/src/main/cpp/synth/SynthTools.h b/apps/OboeTester/app/src/main/cpp/synth/SynthTools.h
new file mode 100644
index 00000000..807b709b
--- /dev/null
+++ b/apps/OboeTester/app/src/main/cpp/synth/SynthTools.h
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2016 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 SYNTHMARK_SYNTHTOOLS_H
+#define SYNTHMARK_SYNTHTOOLS_H
+
+#include <cmath>
+#include <cstdint>
+
+namespace marksynth {
+typedef float synth_float_t;
+
+// The number of frames that are synthesized at one time.
+constexpr int kSynthmarkFramesPerRender = 8;
+
+constexpr int kSynthmarkSampleRate = 48000;
+constexpr int kSynthmarkMaxVoices = 1024;
+
+/**
+ * A fractional amplitude corresponding to exactly -96 dB.
+ * amplitude = pow(10.0, db/20.0)
+ */
+constexpr double kAmplitudeDb96 = 1.0 / 63095.73444801943;
+
+/** A fraction that is approximately -90.3 dB. Defined as 1 bit of an S16. */
+constexpr double kAmplitudeDb90 = 1.0 / (1 << 15);
+
+class SynthTools
+{
+public:
+
+ static void fillBuffer(synth_float_t *output,
+ int32_t numSamples,
+ synth_float_t value) {
+ for (int i = 0; i < numSamples; i++) {
+ *output++ = value;
+ }
+ }
+
+ static void scaleBuffer(const synth_float_t *input,
+ synth_float_t *output,
+ int32_t numSamples,
+ synth_float_t multiplier) {
+ for (int i = 0; i < numSamples; i++) {
+ *output++ = *input++ * multiplier;
+ }
+ }
+
+ static void scaleOffsetBuffer(const synth_float_t *input,
+ synth_float_t *output,
+ int32_t numSamples,
+ synth_float_t multiplier,
+ synth_float_t offset) {
+ for (int i = 0; i < numSamples; i++) {
+ *output++ = (*input++ * multiplier) + offset;
+ }
+ }
+
+ static void mixBuffers(const synth_float_t *input1,
+ synth_float_t gain1,
+ const synth_float_t *input2,
+ synth_float_t gain2,
+ synth_float_t *output,
+ int32_t numSamples) {
+ for (int i = 0; i < numSamples; i++) {
+ *output++ = (*input1++ * gain1) + (*input2++ * gain2);
+ }
+ }
+
+ static void multiplyBuffers(const synth_float_t *input1,
+ const synth_float_t *input2,
+ synth_float_t *output,
+ int32_t numSamples) {
+ for (int i = 0; i < numSamples; i++) {
+ *output++ = *input1++ * *input2;
+ }
+ }
+
+ static double convertTimeToExponentialScaler(synth_float_t duration, synth_float_t sampleRate) {
+ // Calculate scaler so that scaler^frames = target/source
+ synth_float_t numFrames = duration * sampleRate;
+ return pow(kAmplitudeDb90, (1.0 / numFrames));
+ }
+
+ /**
+ * Calculate sine using a Taylor expansion.
+ * Code is based on SineOscillator from JSyn.
+ *
+ * @param phase between -PI and +PI
+ */
+ static synth_float_t fastSine(synth_float_t phase) {
+ // Factorial coefficients.
+ const synth_float_t IF3 = 1.0 / (2 * 3);
+ const synth_float_t IF5 = IF3 / (4 * 5);
+ const synth_float_t IF7 = IF5 / (6 * 7);
+ const synth_float_t IF9 = IF7 / (8 * 9);
+ const synth_float_t IF11 = IF9 / (10 * 11);
+
+ /* Wrap phase back into region where results are more accurate. */
+ synth_float_t x = (phase > M_PI_2) ? M_PI - phase
+ : ((phase < -M_PI_2) ? -(M_PI + phase) : phase);
+
+ synth_float_t x2 = (x * x);
+ /* Taylor expansion out to x**11/11! factored into multiply-adds */
+ return x * (x2 * (x2 * (x2 * (x2 * ((x2 * (-IF11)) + IF9) - IF7) + IF5) - IF3) + 1);
+ }
+
+ /**
+ * Calculate cosine using a Taylor expansion.
+ *
+ * @param phase between -PI and +PI
+ */
+ static synth_float_t fastCosine(synth_float_t phase) {
+ // Factorial coefficients.
+ const synth_float_t IF2 = 1.0 / (2);
+ const synth_float_t IF4 = IF2 / (3 * 4);
+ const synth_float_t IF6 = IF4 / (5 * 6);
+ const synth_float_t IF8 = IF6 / (7 * 8);
+ const synth_float_t IF10 = IF8 / (9 * 10);
+
+ /* Wrap phase back into region where results are more accurate. */
+ synth_float_t x = phase;
+ if (x < 0.0) {
+ x = 0.0 - phase;
+ }
+ int negate = 1;
+ if (x > M_PI_2) {
+ x = M_PI_2 - x;
+ negate = -1;
+ }
+
+ synth_float_t x2 = (x * x);
+ /* Taylor expansion out to x**11/11! factored into multiply-adds */
+ synth_float_t cosine =
+ 1 + (x2 * (x2 * (x2 * (x2 * ((x2 * (-IF10)) + IF8) - IF6) + IF4) - IF2));
+ return cosine * negate;
+ }
+
+
+ /**
+ * Calculate random 32 bit number using linear-congruential method.
+ */
+ static uint32_t nextRandomInteger() {
+ static uint64_t seed = 99887766;
+ // Use values for 64-bit sequence from MMIX by Donald Knuth.
+ seed = (seed * 6364136223846793005L) + 1442695040888963407L;
+ return (uint32_t) (seed >> 32); // The higher bits have a longer sequence.
+ }
+
+ /**
+ * @return a random double between 0.0 and 1.0
+ */
+ static double nextRandomDouble() {
+ const double scaler = 1.0 / (((uint64_t)1) << 32);
+ return nextRandomInteger() * scaler;
+ }
+
+};
+};
+#endif // SYNTHMARK_SYNTHTOOLS_H
diff --git a/apps/OboeTester/app/src/main/cpp/synth/Synthesizer.h b/apps/OboeTester/app/src/main/cpp/synth/Synthesizer.h
new file mode 100644
index 00000000..e9e1e949
--- /dev/null
+++ b/apps/OboeTester/app/src/main/cpp/synth/Synthesizer.h
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2016 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 SYNTHMARK_SYNTHESIZER_H
+#define SYNTHMARK_SYNTHESIZER_H
+
+#include <cstdint>
+#include <math.h>
+#include <memory>
+#include <string.h>
+#include <cassert>
+#include "SynthTools.h"
+#include "VoiceBase.h"
+#include "SimpleVoice.h"
+
+namespace marksynth {
+#define SAMPLES_PER_FRAME 2
+
+/**
+ * Manage an array of voices.
+ * Note that this is not a fully featured general purpose synthesizer.
+ * It is designed simply to have a similar CPU load as a common synthesizer.
+ */
+class Synthesizer
+{
+public:
+ Synthesizer()
+ : mMaxVoices(0)
+ , mActiveVoiceCount(0)
+ , mVoices(NULL)
+ {}
+
+ virtual ~Synthesizer() {
+ delete[] mVoices;
+ };
+
+ int32_t setup(int32_t sampleRate, int32_t maxVoices) {
+ mMaxVoices = maxVoices;
+ UnitGenerator::setSampleRate(sampleRate);
+ mVoices = new SimpleVoice[mMaxVoices];
+ return (mVoices == NULL) ? -1 : 0;
+ }
+
+ void allNotesOn() {
+ notesOn(mMaxVoices);
+ }
+
+ int32_t notesOn(int32_t numVoices) {
+ if (numVoices > mMaxVoices) {
+ return -1;
+ }
+ mActiveVoiceCount = numVoices;
+ // Leave some headroom so the resonant filter does not clip.
+ mVoiceAmplitude = 0.5f / sqrt(mActiveVoiceCount);
+
+ int pitchIndex = 0;
+ synth_float_t pitches[] = {60.0, 64.0, 67.0, 69.0};
+ for(int iv = 0; iv < mActiveVoiceCount; iv++ ) {
+ SimpleVoice *voice = &mVoices[iv];
+ // Randomize pitches by a few cents to smooth out the CPU load.
+ float pitchOffset = 0.03f * (float) SynthTools::nextRandomDouble();
+ synth_float_t pitch = pitches[pitchIndex++] + pitchOffset;
+ if (pitchIndex > 3) pitchIndex = 0;
+ voice->noteOn(pitch, 1.0);
+ }
+ return 0;
+ }
+
+ void allNotesOff() {
+ for(int iv = 0; iv < mActiveVoiceCount; iv++ ) {
+ SimpleVoice *voice = &mVoices[iv];
+ voice->noteOff();
+ }
+ }
+
+ void renderStereo(float *output, int32_t numFrames) {
+ int32_t framesLeft = numFrames;
+ float *renderBuffer = output;
+
+ // Clear mixing buffer.
+ memset(output, 0, numFrames * SAMPLES_PER_FRAME * sizeof(float));
+
+ while (framesLeft > 0) {
+ int framesThisTime = std::min(kSynthmarkFramesPerRender, framesLeft);
+ for(int iv = 0; iv < mActiveVoiceCount; iv++ ) {
+ SimpleVoice *voice = &mVoices[iv];
+ voice->generate(framesThisTime);
+ float *mix = renderBuffer;
+
+ synth_float_t leftGain = mVoiceAmplitude;
+ synth_float_t rightGain = mVoiceAmplitude;
+ if (mActiveVoiceCount > 1) {
+ synth_float_t pan = iv / (mActiveVoiceCount - 1.0f);
+ leftGain *= pan;
+ rightGain *= 1.0 - pan;
+ }
+ for(int n = 0; n < kSynthmarkFramesPerRender; n++ ) {
+ synth_float_t sample = voice->output[n];
+ *mix++ += (float) (sample * leftGain);
+ *mix++ += (float) (sample * rightGain);
+ }
+ }
+ framesLeft -= framesThisTime;
+ mFrameCounter += framesThisTime;
+ renderBuffer += framesThisTime * SAMPLES_PER_FRAME;
+ }
+ assert(framesLeft == 0);
+ }
+
+ int32_t getActiveVoiceCount() {
+ return mActiveVoiceCount;
+ }
+
+private:
+ int32_t mMaxVoices;
+ int32_t mActiveVoiceCount;
+ int64_t mFrameCounter;
+ SimpleVoice *mVoices;
+ synth_float_t mVoiceAmplitude = 1.0;
+};
+};
+#endif // SYNTHMARK_SYNTHESIZER_H
diff --git a/apps/OboeTester/app/src/main/cpp/synth/UnitGenerator.h b/apps/OboeTester/app/src/main/cpp/synth/UnitGenerator.h
new file mode 100644
index 00000000..3b5a597c
--- /dev/null
+++ b/apps/OboeTester/app/src/main/cpp/synth/UnitGenerator.h
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2016 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.
+ *
+ * This code was translated from the JSyn Java code.
+ * JSyn is Copyright 2009 Phil Burk, Mobileer Inc
+ * JSyn is licensed under the Apache License, Version 2.0
+ */
+
+#ifndef SYNTHMARK_UNIT_GENERATOR_H
+#define SYNTHMARK_UNIT_GENERATOR_H
+
+#include <cstdint>
+#include <assert.h>
+#include <math.h>
+#include "SynthTools.h"
+//#include "DifferentiatedParabola.h"
+
+namespace marksynth {
+class UnitGenerator
+{
+public:
+ UnitGenerator() {}
+
+ virtual ~UnitGenerator() = default;
+
+ static void setSampleRate(int32_t sampleRate) {
+ assert(sampleRate > 0);
+ mSampleRate = sampleRate;
+ mSamplePeriod = 1.0f / sampleRate;
+ }
+
+ static int32_t getSampleRate() {
+ return mSampleRate;
+ }
+
+ synth_float_t output[kSynthmarkFramesPerRender];
+
+public:
+ static int32_t mSampleRate;
+ static synth_float_t mSamplePeriod;
+};
+}
+#endif // SYNTHMARK_UNIT_GENERATOR_H
diff --git a/apps/OboeTester/app/src/main/cpp/synth/VoiceBase.h b/apps/OboeTester/app/src/main/cpp/synth/VoiceBase.h
new file mode 100644
index 00000000..b77b637b
--- /dev/null
+++ b/apps/OboeTester/app/src/main/cpp/synth/VoiceBase.h
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2016 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 SYNTHMARK_VOICE_BASE_H
+#define SYNTHMARK_VOICE_BASE_H
+
+#include <cstdint>
+#include "SynthTools.h"
+#include "UnitGenerator.h"
+
+namespace marksynth {
+/**
+ * Base class for building synthesizers.
+ */
+class VoiceBase : public UnitGenerator
+{
+public:
+ VoiceBase()
+ : mPitch(60.0) // MIDI Middle C is 60
+ , mVelocity(1.0) // normalized
+ {
+ }
+
+ virtual ~VoiceBase() = default;
+
+ void setPitch(synth_float_t pitch) {
+ mPitch = pitch;
+ }
+
+ void noteOn(synth_float_t pitch, synth_float_t velocity) {
+ mVelocity = velocity;
+ mPitch = pitch;
+ }
+
+ void noteOff() {
+ }
+
+ virtual void generate(int32_t numFrames) = 0;
+
+protected:
+ synth_float_t mPitch;
+ synth_float_t mVelocity;
+};
+};
+#endif // SYNTHMARK_VOICE_BASE_H
diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/audio_device/AudioDeviceAdapter.java b/apps/OboeTester/app/src/main/java/com/mobileer/audio_device/AudioDeviceAdapter.java
index 6e22da99..6444b2a7 100644
--- a/apps/OboeTester/app/src/main/java/com/mobileer/audio_device/AudioDeviceAdapter.java
+++ b/apps/OboeTester/app/src/main/java/com/mobileer/audio_device/AudioDeviceAdapter.java
@@ -16,13 +16,13 @@ package com.mobileer.audio_device;
*/
import android.content.Context;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import com.mobileer.oboetester.R;
diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/audio_device/AudioDeviceInfoConverter.java b/apps/OboeTester/app/src/main/java/com/mobileer/audio_device/AudioDeviceInfoConverter.java
index d0228fdb..14c635d2 100644
--- a/apps/OboeTester/app/src/main/java/com/mobileer/audio_device/AudioDeviceInfoConverter.java
+++ b/apps/OboeTester/app/src/main/java/com/mobileer/audio_device/AudioDeviceInfoConverter.java
@@ -15,7 +15,13 @@ package com.mobileer.audio_device;
* limitations under the License.
*/
+import android.media.AudioDescriptor;
import android.media.AudioDeviceInfo;
+import android.media.AudioProfile;
+import android.os.Build;
+
+import java.util.List;
+import java.util.Locale;
public class AudioDeviceInfoConverter {
@@ -49,11 +55,11 @@ public class AudioDeviceInfoConverter {
sb.append("\nChannel masks: ");
int[] channelMasks = adi.getChannelMasks();
- sb.append(intArrayToString(channelMasks));
+ sb.append(intArrayToStringHex(channelMasks));
sb.append("\nChannel index masks: ");
int[] channelIndexMasks = adi.getChannelIndexMasks();
- sb.append(intArrayToString(channelIndexMasks));
+ sb.append(intArrayToStringHex(channelIndexMasks));
sb.append("\nEncodings: ");
int[] encodings = adi.getEncodings();
@@ -62,6 +68,36 @@ public class AudioDeviceInfoConverter {
sb.append("\nSample Rates: ");
int[] sampleRates = adi.getSampleRates();
sb.append(intArrayToString(sampleRates));
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ sb.append("\nAddress: ");
+ sb.append(adi.getAddress());
+ }
+
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
+ sb.append("\nEncapsulation Metadata Types: ");
+ int[] encapsulationMetadataTypes = adi.getEncapsulationMetadataTypes();
+ sb.append(intArrayToString(encapsulationMetadataTypes));
+ }
+
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
+ sb.append("\nEncapsulation Modes: ");
+ int[] encapsulationModes = adi.getEncapsulationModes();
+ sb.append(intArrayToString(encapsulationModes));
+ }
+
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
+ sb.append("\nAudio Descriptors: ");
+ List<AudioDescriptor> audioDescriptors = adi.getAudioDescriptors();
+ sb.append(audioDescriptors);
+ }
+
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
+ sb.append("\nAudio Profiles: ");
+ List<AudioProfile> audioProfiles = adi.getAudioProfiles();
+ sb.append(audioProfiles);
+ }
+
sb.append("\n");
return sb.toString();
}
@@ -76,7 +112,22 @@ public class AudioDeviceInfoConverter {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < integerArray.length; i++){
sb.append(integerArray[i]);
- if (i != integerArray.length -1) sb.append(" ");
+ if (i != integerArray.length - 1) sb.append(" ");
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Converts an integer array into a hexadecimal string where each int is separated by a space
+ *
+ * @param integerArray the integer array to convert to a string
+ * @return string containing all the integer values separated by spaces
+ */
+ private static String intArrayToStringHex(int[] integerArray){
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < integerArray.length; i++){
+ sb.append(String.format(Locale.getDefault(), "0x%02X", integerArray[i]));
+ if (i != integerArray.length - 1) sb.append(" ");
}
return sb.toString();
}
diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/audio_device/AudioDeviceListEntry.java b/apps/OboeTester/app/src/main/java/com/mobileer/audio_device/AudioDeviceListEntry.java
index a0ea1837..baceb475 100644
--- a/apps/OboeTester/app/src/main/java/com/mobileer/audio_device/AudioDeviceListEntry.java
+++ b/apps/OboeTester/app/src/main/java/com/mobileer/audio_device/AudioDeviceListEntry.java
@@ -32,9 +32,16 @@ public class AudioDeviceListEntry {
private int mId;
private String mName;
- public AudioDeviceListEntry(int deviceId, String deviceName){
+ private AudioDeviceInfo mDeviceInfo;
+
+ public AudioDeviceListEntry(int deviceId, String deviceName) {
+ this(deviceId, deviceName, null);
+ }
+
+ public AudioDeviceListEntry(int deviceId, String deviceName, AudioDeviceInfo deviceInfo) {
mId = deviceId;
mName = deviceName;
+ mDeviceInfo = deviceInfo;
}
public int getId() {
@@ -45,6 +52,8 @@ public class AudioDeviceListEntry {
return mName;
}
+ public AudioDeviceInfo getDeviceInfo() { return mDeviceInfo; }
+
public String toString(){
return getName();
}
@@ -87,7 +96,8 @@ public class AudioDeviceListEntry {
listEntries.add(new AudioDeviceListEntry(info.getId(),
info.getId() + ": " +
info.getProductName() + " " +
- AudioDeviceInfoConverter.typeToString(info.getType())));
+ AudioDeviceInfoConverter.typeToString(info.getType()),
+ info));
}
}
return listEntries;
diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/audio_device/CommunicationDeviceSpinner.java b/apps/OboeTester/app/src/main/java/com/mobileer/audio_device/CommunicationDeviceSpinner.java
index fcdcf412..46a924f5 100644
--- a/apps/OboeTester/app/src/main/java/com/mobileer/audio_device/CommunicationDeviceSpinner.java
+++ b/apps/OboeTester/app/src/main/java/com/mobileer/audio_device/CommunicationDeviceSpinner.java
@@ -29,8 +29,10 @@ import com.mobileer.oboetester.R;
import java.util.List;
public class CommunicationDeviceSpinner extends Spinner {
- private static final int CLEAR_DEVICE_ID = 0;
private static final String TAG = CommunicationDeviceSpinner.class.getName();
+ // menu positions
+ public static final int POS_CLEAR = 0;
+ public static final int POS_DEVICES = 1; // base position for device list
private AudioDeviceAdapter mDeviceAdapter;
private AudioManager mAudioManager;
private Context mContext;
@@ -85,10 +87,9 @@ public class CommunicationDeviceSpinner extends Spinner {
mDeviceAdapter = new AudioDeviceAdapter(context);
setAdapter(mDeviceAdapter);
- // Add a default entry to the list and select it
- mDeviceAdapter.add(new AudioDeviceListEntry(CLEAR_DEVICE_ID,
- mContext.getString(R.string.auto_select)));
- setSelection(0);
+ // Add default entries to the list and select one.
+ addDefaultDevicesOptions();
+ setSelection(POS_CLEAR);
setupCommunicationDeviceListener();
}
@@ -108,9 +109,8 @@ public class CommunicationDeviceSpinner extends Spinner {
private void updateDeviceList() {
mDeviceAdapter.clear();
- mDeviceAdapter.add(new AudioDeviceListEntry(CLEAR_DEVICE_ID,
- mContext.getString(R.string.clear)));
- setSelection(0);
+ addDefaultDevicesOptions();
+ setSelection(POS_CLEAR);
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
List<AudioDeviceInfo> commDeviceList = mAudioManager.getAvailableCommunicationDevices();
mCommDeviceArray = commDeviceList.toArray(new AudioDeviceInfo[0]);
@@ -123,4 +123,9 @@ public class CommunicationDeviceSpinner extends Spinner {
}
}, null);
}
+
+ private void addDefaultDevicesOptions() {
+ mDeviceAdapter.add(new AudioDeviceListEntry(POS_CLEAR,
+ mContext.getString(R.string.clear_comm)));
+ }
}
diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/miditools/synth/SynthEngine.java b/apps/OboeTester/app/src/main/java/com/mobileer/miditools/synth/SynthEngine.java
index 1e1c3a8b..da5d7512 100644
--- a/apps/OboeTester/app/src/main/java/com/mobileer/miditools/synth/SynthEngine.java
+++ b/apps/OboeTester/app/src/main/java/com/mobileer/miditools/synth/SynthEngine.java
@@ -27,6 +27,7 @@ import java.io.IOException;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.Iterator;
+import java.util.Locale;
/**
* Very simple polyphonic, single channel synthesizer. It runs a background
@@ -178,7 +179,7 @@ public class SynthEngine extends MidiReceiver {
public void logMidiMessage(byte[] data, int offset, int count) {
String text = "Received: ";
for (int i = 0; i < count; i++) {
- text += String.format("0x%02X, ", data[offset + i]);
+ text += String.format(Locale.getDefault(), "0x%02X, ", data[offset + i]);
}
Log.i(TAG, text);
}
diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AnalyzerActivity.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AnalyzerActivity.java
index 368d5bc8..5e22c0bc 100644
--- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AnalyzerActivity.java
+++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AnalyzerActivity.java
@@ -21,16 +21,17 @@ import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
-import android.support.annotation.NonNull;
-import android.support.v4.app.ActivityCompat;
-import android.support.v4.content.ContextCompat;
import android.widget.Toast;
+import androidx.annotation.NonNull;
+import androidx.core.app.ActivityCompat;
+import androidx.core.content.ContextCompat;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
+import java.util.Locale;
/**
* Activity to measure latency on a full duplex stream.
@@ -78,8 +79,8 @@ public class AnalyzerActivity extends TestInputActivity {
report.append("build.fingerprint = " + Build.FINGERPRINT + "\n");
try {
PackageInfo pinfo = getPackageManager().getPackageInfo(getPackageName(), 0);
- report.append(String.format("test.version = %s\n", pinfo.versionName));
- report.append(String.format("test.version.code = %d\n", pinfo.versionCode));
+ report.append(String.format(Locale.getDefault(), "test.version = %s\n", pinfo.versionName));
+ report.append(String.format(Locale.getDefault(), "test.version.code = %d\n", pinfo.versionCode));
} catch (PackageManager.NameNotFoundException e) {
}
report.append("time.millis = " + System.currentTimeMillis() + "\n");
@@ -87,18 +88,18 @@ public class AnalyzerActivity extends TestInputActivity {
// INPUT
report.append(mAudioInputTester.actualConfiguration.dump());
AudioStreamBase inStream = mAudioInputTester.getCurrentAudioStream();
- report.append(String.format("in.burst.frames = %d\n", inStream.getFramesPerBurst()));
- report.append(String.format("in.xruns = %d\n", inStream.getXRunCount()));
+ report.append(String.format(Locale.getDefault(), "in.burst.frames = %d\n", inStream.getFramesPerBurst()));
+ report.append(String.format(Locale.getDefault(), "in.xruns = %d\n", inStream.getXRunCount()));
// OUTPUT
report.append(mAudioOutTester.actualConfiguration.dump());
AudioStreamBase outStream = mAudioOutTester.getCurrentAudioStream();
- report.append(String.format("out.burst.frames = %d\n", outStream.getFramesPerBurst()));
+ report.append(String.format(Locale.getDefault(), "out.burst.frames = %d\n", outStream.getFramesPerBurst()));
int bufferSize = outStream.getBufferSizeInFrames();
- report.append(String.format("out.buffer.size.frames = %d\n", bufferSize));
+ report.append(String.format(Locale.getDefault(), "out.buffer.size.frames = %d\n", bufferSize));
int bufferCapacity = outStream.getBufferCapacityInFrames();
- report.append(String.format("out.buffer.capacity.frames = %d\n", bufferCapacity));
- report.append(String.format("out.xruns = %d\n", outStream.getXRunCount()));
+ report.append(String.format(Locale.getDefault(), "out.buffer.capacity.frames = %d\n", bufferCapacity));
+ report.append(String.format(Locale.getDefault(), "out.xruns = %d\n", outStream.getXRunCount()));
return report.toString();
}
@@ -160,11 +161,4 @@ public class AnalyzerActivity extends TestInputActivity {
}
}
- private void writeTestInBackground(final String resultString) {
- new Thread() {
- public void run() {
- writeTestResult(resultString);
- }
- }.start();
- }
}
diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioOutputTester.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioOutputTester.java
index 2c305fb5..04c3f2e3 100644
--- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioOutputTester.java
+++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioOutputTester.java
@@ -51,6 +51,10 @@ public class AudioOutputTester extends AudioStreamTester {
mOboeAudioOutputStream.setSignalType(type);
}
+ public void setAmplitude(float amplitude) {
+ mOboeAudioOutputStream.setAmplitude(amplitude);
+ }
+
public int getLastErrorCallbackResult() {return mOboeAudioOutputStream.getLastErrorCallbackResult();};
public long getFramesRead() {return mOboeAudioOutputStream.getFramesRead();};
diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioQueryTools.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioQueryTools.java
index 421d098b..a4dd6fc4 100644
--- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioQueryTools.java
+++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioQueryTools.java
@@ -23,6 +23,7 @@ import android.os.Build;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
+import java.util.Locale;
public class AudioQueryTools {
private static String GETPROP_EXECUTABLE_PATH = "/system/bin/getprop";
@@ -58,6 +59,10 @@ public class AudioQueryTools {
+ packageManager.hasSystemFeature(PackageManager.FEATURE_AUDIO_PRO));
report.append("\nLowLatency Feature : "
+ packageManager.hasSystemFeature(PackageManager.FEATURE_AUDIO_LOW_LATENCY));
+ report.append("\nAudio Output Feature : "
+ + packageManager.hasSystemFeature(PackageManager.FEATURE_AUDIO_OUTPUT));
+ report.append("\nMicrophone Feature : "
+ + packageManager.hasSystemFeature(PackageManager.FEATURE_MICROPHONE));
report.append("\nMIDI Feature : "
+ packageManager.hasSystemFeature(PackageManager.FEATURE_MIDI));
report.append("\nUSB Host Feature : "
@@ -69,14 +74,24 @@ public class AudioQueryTools {
public static String getAudioManagerReport(AudioManager audioManager) {
StringBuffer report = new StringBuffer();
- String unprocessedSupport = audioManager.getParameters(AudioManager.PROPERTY_SUPPORT_AUDIO_SOURCE_UNPROCESSED);
- report.append("\nSUPPORT_UNPROCESSED : " + ((unprocessedSupport == null) ? "null" : "yes"));
+ String unprocessedSupport = audioManager.getProperty(
+ AudioManager.PROPERTY_SUPPORT_AUDIO_SOURCE_UNPROCESSED);
+ report.append("\nSUPPORT_AUDIO_SOURCE_UNPROCESSED : " + ((unprocessedSupport == null) ?
+ "null" : unprocessedSupport));
+ String outputFramesPerBuffer = audioManager.getProperty(
+ AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER);
+ report.append("\nOUTPUT_FRAMES_PER_BUFFER : " + ((outputFramesPerBuffer == null) ?
+ "null" : outputFramesPerBuffer));
+ String outputSampleRate = audioManager.getProperty(
+ AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE);
+ report.append("\nOUTPUT_SAMPLE_RATE : " + ((outputSampleRate == null) ? "null" :
+ outputSampleRate));
return report.toString();
}
private static String formatKeyValueLine(String key, String value) {
int numSpaces = Math.max(1, 21 - key.length());
- String spaces = String.format("%0" + numSpaces + "d", 0).replace("0", " ");
+ String spaces = String.format(Locale.getDefault(), "%0" + numSpaces + "d", 0).replace("0", " ");
return "\n" + key + spaces + ": " + value;
}
@@ -112,6 +127,9 @@ public class AudioQueryTools {
}
public static String getMediaPerformanceClass() {
+ if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.S) {
+ return formatKeyValueLine("Media Perf Class", "not supported");
+ }
int mpc = Build.VERSION.MEDIA_PERFORMANCE_CLASS;
String text = (mpc == 0) ? "not declared" : convertSdkToShortName(mpc);
return formatKeyValueLine("Media Perf Class",
diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioRecordThread.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioRecordThread.java
index 7b472237..bc254552 100644
--- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioRecordThread.java
+++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioRecordThread.java
@@ -17,6 +17,7 @@
package com.mobileer.oboetester;
+import android.media.AudioDeviceInfo;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaRecorder;
@@ -40,6 +41,8 @@ class AudioRecordThread implements Runnable {
private int mTaskCountdown;
private boolean mCaptureEnabled = true;
+ private AudioDeviceInfo mDeviceInfo;
+
public AudioRecordThread(int frameRate, int channelCount, int maxFrames) {
mSampleRate = frameRate;
mChannelCount = channelCount;
@@ -59,6 +62,7 @@ class AudioRecordThread implements Runnable {
channelConfig,
audioFormat,
2 * minRecordBuffSizeInBytes);
+ mRecorder.setPreferredDevice(mDeviceInfo);
if (mRecorder.getState() == AudioRecord.STATE_UNINITIALIZED) {
throw new RuntimeException("Could not make the AudioRecord - UNINITIALIZED");
}
@@ -159,4 +163,7 @@ class AudioRecordThread implements Runnable {
return mCaptureBuffer.readMostRecent(buffer);
}
+ public void setInputDevice(AudioDeviceInfo deviceInfo) {
+ mDeviceInfo = deviceInfo;
+ }
}
diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioStreamBase.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioStreamBase.java
index b6eb6ff1..14eb163e 100644
--- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioStreamBase.java
+++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioStreamBase.java
@@ -17,6 +17,7 @@
package com.mobileer.oboetester;
import java.io.IOException;
+import java.util.Locale;
/**
* Base class for any audio input or output.
@@ -25,10 +26,43 @@ public abstract class AudioStreamBase {
private StreamConfiguration mRequestedStreamConfiguration;
private StreamConfiguration mActualStreamConfiguration;
- AudioStreamBase.DoubleStatistics mLatencyStatistics;
-
+ private AudioStreamBase.DoubleStatistics mLatencyStatistics;
+ private SampleRateMonitor mSampleRateMonitor = new SampleRateMonitor();
private int mBufferSizeInFrames;
+ private class SampleRateMonitor {
+ private static final int SIZE = 16; // power of 2
+ private static final long MASK = SIZE - 1L;
+ private long[] times = new long[SIZE];
+ private long[] frames = new long[SIZE];
+ private long cursor;
+
+ void add(long numFrames) {
+ int index = (int) (cursor & MASK);
+ frames[index] = numFrames;
+ times[index] = System.currentTimeMillis();
+ cursor++;
+ }
+
+ int getRate() {
+ if (cursor < 2) return 0;
+ long numValid = Math.min((long)SIZE, cursor);
+ int oldestIndex = (int)((cursor - numValid) & MASK);
+ int newestIndex = (int)((cursor - 1) & MASK);
+ long deltaTime = times[newestIndex] - times[oldestIndex];
+ long deltaFrames = frames[newestIndex] - frames[oldestIndex];
+ if (deltaTime <= 0) {
+ return -1;
+ }
+ long sampleRate = (deltaFrames * 1000) / deltaTime;
+ return (int) sampleRate;
+ }
+
+ void reset() {
+ cursor = 0;
+ }
+ }
+
public StreamStatus getStreamStatus() {
StreamStatus status = new StreamStatus();
status.bufferSize = getBufferSizeInFrames();
@@ -41,6 +75,8 @@ public abstract class AudioStreamBase {
status.callbackTimeStr = getCallbackTimeStr();
status.cpuLoad = getCpuLoad();
status.state = getState();
+ mSampleRateMonitor.add(status.framesRead);
+ status.measuredRate = mSampleRateMonitor.getRate();
return status;
}
@@ -48,6 +84,11 @@ public abstract class AudioStreamBase {
return mLatencyStatistics;
}
+ public void setPerformanceHintEnabled(boolean checked) {
+ }
+ public void setHearWorkload(boolean checked) {
+ }
+
public static class DoubleStatistics {
private double sum;
private int count;
@@ -68,7 +109,7 @@ public abstract class AudioStreamBase {
public String dump() {
if (count == 0) return "?";
- return String.format("%3.1f/%3.1f/%3.1f ms", minimum, getAverage(), maximum);
+ return String.format(Locale.getDefault(), "%3.1f/%3.1f/%3.1f ms", minimum, getAverage(), maximum);
}
}
@@ -84,8 +125,9 @@ public abstract class AudioStreamBase {
public int state;
public long callbackCount;
public int framesPerCallback;
- public double cpuLoad;
+ public float cpuLoad;
public String callbackTimeStr;
+ public int measuredRate;
// These are constantly changing.
String dump(int framesPerBurst) {
@@ -96,21 +138,22 @@ public abstract class AudioStreamBase {
buffer.append("time between callbacks = " + callbackTimeStr + "\n");
- buffer.append("written "
- + String.format("0x%08X", framesWritten)
- + " - read " + String.format("0x%08X", framesRead)
- + " = " + (framesWritten - framesRead) + " frames\n");
+ buffer.append("wr "
+ + String.format(Locale.getDefault(), "%Xh", framesWritten)
+ + " - rd " + String.format(Locale.getDefault(), "%Xh", framesRead)
+ + " = " + (framesWritten - framesRead) + " fr"
+ + ", SR = " + ((measuredRate <= 0) ? "?" : measuredRate) + "\n");
- String cpuLoadText = String.format("%2d%c", (int)(cpuLoad * 100), '%');
+ String cpuLoadText = String.format(Locale.getDefault(), "%2d%c", (int)(cpuLoad * 100), '%');
buffer.append(
convertStateToString(state)
+ ", #cb=" + callbackCount
- + ", f/cb=" + String.format("%3d", framesPerCallback)
- + ", " + cpuLoadText + " cpu"
+ + ", f/cb=" + String.format(Locale.getDefault(), "%3d", framesPerCallback)
+ + ", " + cpuLoadText + " CPU"
+ "\n");
buffer.append("buffer size = ");
- if (bufferSize < 0) {
+ if (bufferSize <= 0 || framesPerBurst <= 0) {
buffer.append("?");
} else {
int numBuffers = bufferSize / framesPerBurst;
@@ -151,14 +194,19 @@ public abstract class AudioStreamBase {
mLatencyStatistics = new AudioStreamBase.DoubleStatistics();
}
+ public void onStart() {
+ mSampleRateMonitor.reset();
+ }
+ public void onStop() {
+ mSampleRateMonitor.reset();
+ }
+
public abstract boolean isInput();
public void startPlayback() throws IOException {}
public void stopPlayback() throws IOException {}
- public abstract int write(float[] buffer, int offset, int length);
-
public abstract void close();
public int getChannelCount() {
@@ -195,17 +243,15 @@ public abstract class AudioStreamBase {
public double getLatency() { return -1.0; }
- public double getCpuLoad() { return 0.0; }
+ public float getCpuLoad() { return 0.0f; }
+ public float getAndResetMaxCpuLoad() { return 0.0f; }
+ public int getAndResetCpuMask() { return 0; }
public String getCallbackTimeStr() { return "?"; };
public int getState() { return -1; }
- public boolean isThresholdSupported() {
- return false;
- }
-
- public void setWorkload(double workload) {}
+ public void setWorkload(int workload) {}
public abstract int getXRunCount();
diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioStreamTester.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioStreamTester.java
index 32bc2ae6..be740b3c 100644
--- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioStreamTester.java
+++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AudioStreamTester.java
@@ -33,7 +33,7 @@ class AudioStreamTester {
}
public void reset() {
- setWorkload(0.0);
+ setWorkload(0);
requestedConfiguration.reset(); // TODO consider making new ones
actualConfiguration.reset();
}
@@ -46,7 +46,7 @@ class AudioStreamTester {
mCurrentAudioStream.startPlayback();
}
- public void setWorkload(double workload) {
+ public void setWorkload(int workload) {
mCurrentAudioStream.setWorkload(workload);
}
}
diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AutomatedGlitchActivity.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AutomatedGlitchActivity.java
index e8befcde..ae53a7f6 100644
--- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AutomatedGlitchActivity.java
+++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AutomatedGlitchActivity.java
@@ -1,3 +1,19 @@
+/*
+ * 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.mobileer.oboetester;
import android.os.Bundle;
@@ -5,6 +21,12 @@ import android.view.View;
import android.widget.AdapterView;
import android.widget.Spinner;
+/**
+ * Look for glitches with various configurations.
+ * A sine wave is played and continuously recorded using loopback.
+ * An analyzer locks to the phase and magnitude of the detected sine wave.
+ * It then compares the incoming signal with a predicted sine wave.
+ */
public class AutomatedGlitchActivity extends BaseAutoGlitchActivity {
private Spinner mDurationSpinner;
@@ -43,6 +65,8 @@ public class AutomatedGlitchActivity extends BaseAutoGlitchActivity {
mDurationSpinner = (Spinner) findViewById(R.id.spinner_glitch_duration);
mDurationSpinner.setOnItemSelectedListener(new DurationSpinnerListener());
+
+ setAnalyzerText(getString(R.string.auto_glitch_instructions));
}
@Override
@@ -75,9 +99,7 @@ public class AutomatedGlitchActivity extends BaseAutoGlitchActivity {
requestedInConfig.setChannelCount(inChannels);
requestedOutConfig.setChannelCount(outChannels);
- setTolerance(0.3f); // FIXME remove
-
- testConfigurations();
+ testCurrentConfigurations();
}
private void testConfiguration(int performanceMode,
@@ -98,10 +120,17 @@ public class AutomatedGlitchActivity extends BaseAutoGlitchActivity {
mTestResults.clear();
+ // Test with STEREO on both input and output.
+ testConfiguration(StreamConfiguration.PERFORMANCE_MODE_LOW_LATENCY,
+ StreamConfiguration.SHARING_MODE_EXCLUSIVE,
+ UNSPECIFIED, STEREO, STEREO);
+
+ // Test EXCLUSIVE mode with a configuration most likely to work.
testConfiguration(StreamConfiguration.PERFORMANCE_MODE_LOW_LATENCY,
StreamConfiguration.SHARING_MODE_EXCLUSIVE,
UNSPECIFIED);
+ // Test various combinations.
for (int perfMode : PERFORMANCE_MODES) {
for (int sampleRate : SAMPLE_RATES) {
testConfiguration(perfMode,
@@ -110,10 +139,10 @@ public class AutomatedGlitchActivity extends BaseAutoGlitchActivity {
}
}
- analyzeTestResults();
+ compareFailedTestsWithNearestPassingTest();
} catch (InterruptedException e) {
- analyzeTestResults();
+ compareFailedTestsWithNearestPassingTest();
} catch (Exception e) {
log(e.getMessage());
diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AutomatedTestRunner.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AutomatedTestRunner.java
index 83417809..27fc0456 100644
--- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AutomatedTestRunner.java
+++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/AutomatedTestRunner.java
@@ -7,6 +7,7 @@ import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
+import android.view.WindowManager;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.ScrollView;
@@ -184,8 +185,9 @@ public class AutomatedTestRunner extends LinearLayout implements Runnable {
private void stopAutoThread() {
try {
if (mAutoThread != null) {
- log("Disable background test thread.");
- new RuntimeException("Disable background test thread.").printStackTrace();
+ Log.d(TestAudioActivity.TAG,
+ "Who called stopAutoThread()?",
+ new RuntimeException("Just for debugging."));
mThreadEnabled = false;
mAutoThread.interrupt();
mAutoThread.join(100);
@@ -231,7 +233,13 @@ public class AutomatedTestRunner extends LinearLayout implements Runnable {
}
// Only call from UI thread.
+ public void onTestStarted() {
+ mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ }
+
+ // Only call from UI thread.
public void onTestFinished() {
+ mActivity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
updateStartStopButtons(false);
mShareButton.setEnabled(true);
}
@@ -262,6 +270,12 @@ public class AutomatedTestRunner extends LinearLayout implements Runnable {
@Override
public void run() {
+ mActivity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ onTestStarted();
+ }
+ });
logClear();
log("=== STARTED at " + new Date());
log(mActivity.getTestName());
@@ -274,7 +288,7 @@ public class AutomatedTestRunner extends LinearLayout implements Runnable {
mFailCount = 0;
try {
mActivity.runTest();
- log("Tests finished without exception.");
+ log("Tests finished.");
} catch(Exception e) {
log("EXCEPTION: " + e.getMessage());
} finally {
diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/BaseAutoGlitchActivity.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/BaseAutoGlitchActivity.java
index 5bc16fb5..847dc563 100644
--- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/BaseAutoGlitchActivity.java
+++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/BaseAutoGlitchActivity.java
@@ -16,13 +16,18 @@
package com.mobileer.oboetester;
+import static com.mobileer.oboetester.StreamConfiguration.convertChannelMaskToText;
+
import android.content.Context;
+import android.media.AudioDeviceInfo;
import android.media.AudioManager;
import android.os.Bundle;
-import android.support.annotation.Nullable;
+
+import androidx.annotation.Nullable;
import java.io.IOException;
import java.util.ArrayList;
+import java.util.Locale;
public class BaseAutoGlitchActivity extends GlitchActivity {
@@ -37,8 +42,17 @@ public class BaseAutoGlitchActivity extends GlitchActivity {
protected int mGapMillis = DEFAULT_GAP_MILLIS;
private String mTestName = "";
+ protected AudioManager mAudioManager;
+
protected ArrayList<TestResult> mTestResults = new ArrayList<TestResult>();
+ public static boolean arrayContains(int[] haystack, int needle) {
+ for (int n: haystack) {
+ if (n == needle) return true;
+ }
+ return false;
+ }
+
void logDeviceInfo() {
log("\n############################");
log("\nDevice Info:");
@@ -53,27 +67,35 @@ public class BaseAutoGlitchActivity extends GlitchActivity {
mTestName = name;
}
- private static class TestDirection {
+ @Override
+ public int getDeviceId() {
+ return super.getDeviceId();
+ }
+
+ private static class TestStreamOptions {
public final int channelUsed;
public final int channelCount;
+ public final int channelMask;
public final int deviceId;
public final int mmapUsed;
public final int performanceMode;
public final int sharingMode;
- public TestDirection(StreamConfiguration configuration, int channelUsed) {
+ public TestStreamOptions(StreamConfiguration configuration, int channelUsed) {
this.channelUsed = channelUsed;
channelCount = configuration.getChannelCount();
+ channelMask = configuration.getChannelMask();
deviceId = configuration.getDeviceId();
mmapUsed = configuration.isMMap() ? 1 : 0;
performanceMode = configuration.getPerformanceMode();
sharingMode = configuration.getSharingMode();
}
- int countDifferences(TestDirection other) {
+ int countDifferences(TestStreamOptions other) {
int count = 0;
count += (channelUsed != other.channelUsed) ? 1 : 0;
count += (channelCount != other.channelCount) ? 1 : 0;
+ count += (channelMask != other.channelMask) ? 1 : 0;
count += (deviceId != other.deviceId) ? 1 : 0;
count += (mmapUsed != other.mmapUsed) ? 1 : 0;
count += (performanceMode != other.performanceMode) ? 1 : 0;
@@ -81,10 +103,11 @@ public class BaseAutoGlitchActivity extends GlitchActivity {
return count;
}
- public String comparePassedDirection(String prefix, TestDirection passed) {
+ public String comparePassedDirection(String prefix, TestStreamOptions passed) {
StringBuffer text = new StringBuffer();
text.append(TestDataPathsActivity.comparePassedField(prefix, this, passed, "channelUsed"));
text.append(TestDataPathsActivity.comparePassedField(prefix,this, passed, "channelCount"));
+ text.append(TestDataPathsActivity.comparePassedField(prefix,this, passed, "channelMask"));
text.append(TestDataPathsActivity.comparePassedField(prefix,this, passed, "deviceId"));
text.append(TestDataPathsActivity.comparePassedField(prefix,this, passed, "mmapUsed"));
text.append(TestDataPathsActivity.comparePassedField(prefix,this, passed, "performanceMode"));
@@ -96,6 +119,7 @@ public class BaseAutoGlitchActivity extends GlitchActivity {
return "D=" + deviceId
+ ", " + ((mmapUsed > 0) ? "MMAP" : "Lgcy")
+ ", ch=" + channelText(channelUsed, channelCount)
+ + ", cm=" + convertChannelMaskToText(channelMask)
+ "," + StreamConfiguration.convertPerformanceModeToText(performanceMode)
+ "," + StreamConfiguration.convertSharingModeToText(sharingMode);
}
@@ -103,8 +127,8 @@ public class BaseAutoGlitchActivity extends GlitchActivity {
protected static class TestResult {
final int testIndex;
- final TestDirection input;
- final TestDirection output;
+ final TestStreamOptions input;
+ final TestStreamOptions output;
public final int inputPreset;
public final int sampleRate;
final String testName; // name or purpose of test
@@ -120,8 +144,8 @@ public class BaseAutoGlitchActivity extends GlitchActivity {
int outputChannel) {
this.testIndex = testIndex;
this.testName = testName;
- input = new TestDirection(inputConfiguration, inputChannel);
- output = new TestDirection(outputConfiguration, outputChannel);
+ input = new TestStreamOptions(inputConfiguration, inputChannel);
+ output = new TestStreamOptions(outputConfiguration, outputChannel);
sampleRate = outputConfiguration.getSampleRate();
this.inputPreset = inputConfiguration.getInputPreset();
}
@@ -147,7 +171,7 @@ public class BaseAutoGlitchActivity extends GlitchActivity {
StringBuffer text = new StringBuffer();
text.append("Compare with passed test #" + passed.testIndex + "\n");
text.append(input.comparePassedDirection("IN", passed.input));
- text.append(TestDataPathsActivity.comparePassedField("IN", this, passed, "inputPreset"));
+ text.append(TestDataPathsActivity.comparePassedInputPreset("IN", this, passed));
text.append(output.comparePassedDirection("OUT", passed.output));
text.append(TestDataPathsActivity.comparePassedField("I/O",this, passed, "sampleRate"));
@@ -177,6 +201,7 @@ public class BaseAutoGlitchActivity extends GlitchActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+ mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
mAutomatedTestRunner = findViewById(R.id.auto_test_runner);
mAutomatedTestRunner.setActivity(this);
@@ -208,12 +233,12 @@ public class BaseAutoGlitchActivity extends GlitchActivity {
? 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())
+ + "-" + StreamConfiguration.convertSharingModeToText(config.getSharingMode())
+ + ", ID = " + String.format(Locale.getDefault(), "%2d", config.getDeviceId())
+ ", Perf = " + StreamConfiguration.convertPerformanceModeToText(
- config.getPerformanceMode())
- + ", " + StreamConfiguration.convertSharingModeToText(config.getSharingMode())
- + ", ch = " + channelText(channel, config.getChannelCount());
+ config.getPerformanceMode())
+ + ",\n ch = " + channelText(channel, config.getChannelCount())
+ + ", cm = " + convertChannelMaskToText(config.getChannelMask());
}
protected String getStreamText(AudioStreamBase stream) {
@@ -230,15 +255,14 @@ public class BaseAutoGlitchActivity extends GlitchActivity {
// Run one test based on the requested input/output configurations.
@Nullable
- protected TestResult testConfigurations() throws InterruptedException {
- int result = TEST_RESULT_SKIPPED;
+ protected TestResult testCurrentConfigurations() throws InterruptedException {
mAutomatedTestRunner.incrementTestCount();
- if ((getSingleTestIndex() >= 0) && (mAutomatedTestRunner.getTestCount() != getSingleTestIndex())) {
+ if ((getSingleTestIndex() >= 0) && (getTestCount() != getSingleTestIndex())) {
return null;
}
- log("========================== #" + mAutomatedTestRunner.getTestCount());
-
+ log("========================== #" + getTestCount());
+ int result = 0;
StreamConfiguration requestedInConfig = mAudioInputTester.requestedConfiguration;
StreamConfiguration requestedOutConfig = mAudioOutTester.requestedConfiguration;
@@ -246,6 +270,7 @@ public class BaseAutoGlitchActivity extends GlitchActivity {
StreamConfiguration actualOutConfig = mAudioOutTester.actualConfiguration;
log("Requested:");
+ log(" SR = " + requestedOutConfig.getSampleRate());
log(" " + getConfigText(requestedInConfig));
log(" " + getConfigText(requestedOutConfig));
@@ -253,7 +278,9 @@ public class BaseAutoGlitchActivity extends GlitchActivity {
boolean openFailed = false;
try {
openAudio(); // this will fill in actualConfig
+
log("Actual:");
+ log(" SR = " + actualOutConfig.getSampleRate());
// Set output size to a level that will avoid glitches.
AudioStreamBase outStream = mAudioOutTester.getCurrentAudioStream();
int sizeFrames = outStream.getBufferCapacityInFrames() / 2;
@@ -271,7 +298,7 @@ public class BaseAutoGlitchActivity extends GlitchActivity {
}
TestResult testResult = new TestResult(
- mAutomatedTestRunner.getTestCount(),
+ getTestCount(),
mTestName,
mAudioInputTester.actualConfiguration,
getInputChannel(),
@@ -279,14 +306,14 @@ public class BaseAutoGlitchActivity extends GlitchActivity {
getOutputChannel()
);
- // The test would only be worth running if we got the configuration we requested on input or output.
- String skipReason = shouldTestBeSkipped();
+ // The test will only be worth running if we got the configuration we requested on input or output.
+ String skipReason = whyShouldTestBeSkipped();
boolean skipped = skipReason.length() > 0;
boolean valid = !openFailed && !skipped;
boolean startFailed = false;
if (valid) {
try {
- startAudioTest();
+ startAudioTest(); // Start running the test in the background.
} catch (IOException e) {
e.printStackTrace();
valid = false;
@@ -324,7 +351,7 @@ public class BaseAutoGlitchActivity extends GlitchActivity {
}
if (openFailed || startFailed) {
- appendFailedSummary("------ #" + mAutomatedTestRunner.getTestCount() + "\n");
+ appendFailedSummary("------ #" + getTestCount() + "\n");
appendFailedSummary(getConfigText(requestedInConfig) + "\n");
appendFailedSummary(getConfigText(requestedOutConfig) + "\n");
appendFailedSummary(reason + "\n");
@@ -342,7 +369,7 @@ public class BaseAutoGlitchActivity extends GlitchActivity {
resultText += reason;
log(" " + resultText);
if (!passed) {
- appendFailedSummary("------ #" + mAutomatedTestRunner.getTestCount() + "\n");
+ appendFailedSummary("------ #" + getTestCount() + "\n");
appendFailedSummary(" " + getConfigText(actualInConfig) + "\n");
appendFailedSummary(" " + getConfigText(actualOutConfig) + "\n");
appendFailedSummary(" " + resultText + "\n");
@@ -352,6 +379,7 @@ public class BaseAutoGlitchActivity extends GlitchActivity {
mAutomatedTestRunner.incrementPassCount();
result = TEST_RESULT_PASSED;
}
+
}
mAutomatedTestRunner.flushLog();
@@ -362,28 +390,123 @@ public class BaseAutoGlitchActivity extends GlitchActivity {
testResult.setResult(result);
mTestResults.add(testResult);
}
-
return testResult;
}
+ protected int getTestCount() {
+ return mAutomatedTestRunner.getTestCount();
+ }
+
+ protected AudioDeviceInfo getDeviceInfoById(int deviceId) {
+ AudioDeviceInfo[] devices = mAudioManager.getDevices(AudioManager.GET_DEVICES_ALL);
+ for (AudioDeviceInfo deviceInfo : devices) {
+ if (deviceInfo.getId() == deviceId) {
+ return deviceInfo;
+ }
+ }
+ return null;
+ }
+
+ protected AudioDeviceInfo getDeviceInfoByType(int deviceType, int flags) {
+ AudioDeviceInfo[] devices = mAudioManager.getDevices(flags);
+ for (AudioDeviceInfo deviceInfo : devices) {
+ if (deviceInfo.getType() == deviceType) {
+ return deviceInfo;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Are outputs mixed in the air or by a loopback plug?
+ * @param type device type, eg AudioDeviceInfo.TYPE_BUILTIN_SPEAKER
+ * @return true if stereo output channels get mixed to mono input
+ */
+ protected boolean isDeviceTypeMixedForLoopback(int type) {
+ switch(type) {
+ // Mixed in the air.
+ case AudioDeviceInfo.TYPE_BUILTIN_SPEAKER:
+ case AudioDeviceInfo.TYPE_BUILTIN_SPEAKER_SAFE:
+ // Mixed in the loopback fun-plug.
+ case AudioDeviceInfo.TYPE_WIRED_HEADSET:
+ case AudioDeviceInfo.TYPE_USB_HEADSET:
+ return true;
+
+ case AudioDeviceInfo.TYPE_USB_DEVICE:
+ default:
+ return false; // channels are discrete
+ }
+ }
+
+ protected ArrayList<Integer> getCompatibleDeviceTypes(int type) {
+ ArrayList<Integer> compatibleTypes = new ArrayList<Integer>();
+ switch(type) {
+ case AudioDeviceInfo.TYPE_BUILTIN_SPEAKER:
+ case AudioDeviceInfo.TYPE_BUILTIN_SPEAKER_SAFE:
+ compatibleTypes.add(AudioDeviceInfo.TYPE_BUILTIN_MIC);
+ break;
+ case AudioDeviceInfo.TYPE_BUILTIN_MIC:
+ compatibleTypes.add(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER);
+ break;
+ case AudioDeviceInfo.TYPE_USB_DEVICE:
+ compatibleTypes.add(AudioDeviceInfo.TYPE_USB_DEVICE);
+ // A USB Device is often mistaken for a headset.
+ compatibleTypes.add(AudioDeviceInfo.TYPE_USB_HEADSET);
+ break;
+ default:
+ compatibleTypes.add(type);
+ break;
+ }
+ return compatibleTypes;
+ }
+
+ /**
+ * Scan available device for one with a compatible device type for loopback testing.
+ * @return deviceId
+ */
+
+ protected AudioDeviceInfo findCompatibleInputDevice(int outputDeviceType) {
+ ArrayList<Integer> compatibleDeviceTypes = getCompatibleDeviceTypes(outputDeviceType);
+ AudioDeviceInfo[] devices = mAudioManager.getDevices(AudioManager.GET_DEVICES_INPUTS);
+ for (AudioDeviceInfo candidate : devices) {
+ if (compatibleDeviceTypes.contains(candidate.getType())) {
+ return candidate;
+ }
+ }
+ return null;
+ }
+
protected boolean isFinishedEarly() {
return false;
}
- protected String shouldTestBeSkipped() {
+ /**
+ * Figure out if a test should be skipped and return the reason.
+ *
+ * @return reason for skipping or an empty string
+ */
+ protected String whyShouldTestBeSkipped() {
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.
+ // No point running the test if we don't get any of the sharing modes we requested.
if (actualInConfig.getSharingMode() != requestedInConfig.getSharingMode()
- || actualOutConfig.getSharingMode() != requestedOutConfig.getSharingMode()) {
+ && actualOutConfig.getSharingMode() != requestedOutConfig.getSharingMode()) {
log("Did not get requested sharing mode.");
- why += "share";
+ why += "share,";
+ }
+ if (actualInConfig.getPerformanceMode() != requestedInConfig.getPerformanceMode()
+ && actualOutConfig.getPerformanceMode() != requestedOutConfig.getPerformanceMode()) {
+ log("Did not get requested performance mode.");
+ why += "perf,";
+ }
+ if (actualInConfig.isMMap() != requestedInConfig.isMMap()
+ && actualOutConfig.isMMap() != requestedOutConfig.isMMap()) {
+ log("Did not get requested MMAP data path.");
+ why += "mmap,";
}
- // 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;
}
@@ -399,9 +522,23 @@ public class BaseAutoGlitchActivity extends GlitchActivity {
appendFailedSummary(text + "\n");
}
- protected void analyzeTestResults() {
- logAnalysis("\n==== ANALYSIS ===========");
- logAnalysis("Compare failed configuration with closest one that passed.");
+ private int countPassingTests() {
+ int numPassed = 0;
+ for (TestResult other : mTestResults) {
+ if (other.passed()) {
+ numPassed++;
+ }
+ }
+ return numPassed;
+ }
+
+ protected void compareFailedTestsWithNearestPassingTest() {
+ logAnalysis("\n==== COMPARISON ANALYSIS ===========");
+ if (countPassingTests() == 0) {
+ logAnalysis("Comparison skipped because NO tests passed.");
+ return;
+ }
+ logAnalysis("Compare failed tests with others that passed.");
// Analyze each failed test.
for (TestResult testResult : mTestResults) {
if (testResult.failed()) {
@@ -419,6 +556,7 @@ public class BaseAutoGlitchActivity extends GlitchActivity {
}
}
+
@Nullable
private TestResult[] findClosestPassingTestResults(TestResult testResult) {
int minDifferences = Integer.MAX_VALUE;
diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/BaseOboeTesterActivity.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/BaseOboeTesterActivity.java
index 860b8145..4bf57496 100644
--- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/BaseOboeTesterActivity.java
+++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/BaseOboeTesterActivity.java
@@ -20,9 +20,9 @@ import android.Manifest;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageManager;
-import android.support.annotation.NonNull;
-import android.support.v4.app.ActivityCompat;
import android.widget.Toast;
+import androidx.annotation.NonNull;
+import androidx.core.app.ActivityCompat;
/**
* Support requesting RECORD_AUDIO permission.
diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/BufferSizeView.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/BufferSizeView.java
index ce3b7a44..432e0ff3 100644
--- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/BufferSizeView.java
+++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/BufferSizeView.java
@@ -23,6 +23,7 @@ import android.view.LayoutInflater;
import android.view.View;
import android.widget.RadioButton;
+import android.widget.RadioGroup;
import android.widget.SeekBar;
import android.widget.TextView;
import android.widget.LinearLayout;
@@ -36,6 +37,7 @@ public class BufferSizeView extends LinearLayout {
private TextView mTextLabel;
private SeekBar mFader;
private ExponentialTaper mTaper;
+ private RadioGroup mBufferSizeGroup;
private RadioButton mBufferSizeRadio1;
private RadioButton mBufferSizeRadio2;
private RadioButton mBufferSizeRadio3;
@@ -97,6 +99,8 @@ public class BufferSizeView extends LinearLayout {
mTaper = new ExponentialTaper(0.0, 1.0, 10.0);
mFader.setProgress(0);
+ mBufferSizeGroup = (RadioGroup) findViewById(R.id.bufferSizeGroup);
+
mBufferSizeRadio1 = (RadioButton) findViewById(R.id.bufferSize1);
mBufferSizeRadio1.setOnClickListener(new View.OnClickListener() {
@Override
@@ -118,13 +122,18 @@ public class BufferSizeView extends LinearLayout {
onSizeRadioButtonClicked(view, 3);
}
});
+
mNumBursts = DEFAULT_NUM_BURSTS;
updateRadioButtons();
updateBufferSize();
}
public void updateRadioButtons() {
- if (mBufferSizeRadio3 != null) {
+ if (mNumBursts == USE_FADER && mBufferSizeGroup != null) {
+ // Clear all the radio buttons using the group.
+ // If you clear a checked button directly then it stops working.
+ mBufferSizeGroup.clearCheck();
+ } else if (mBufferSizeRadio3 != null) {
mBufferSizeRadio1.setChecked(mNumBursts == 1);
mBufferSizeRadio2.setChecked(mNumBursts == 2);
mBufferSizeRadio3.setChecked(mNumBursts == 3);
diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/CommunicationDeviceView.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/CommunicationDeviceView.java
index 4e8df135..3e4c96df 100644
--- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/CommunicationDeviceView.java
+++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/CommunicationDeviceView.java
@@ -16,7 +16,11 @@
package com.mobileer.oboetester;
+import android.app.Activity;
+import android.content.BroadcastReceiver;
import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
import android.os.Build;
@@ -31,11 +35,27 @@ import android.widget.TextView;
import com.mobileer.audio_device.CommunicationDeviceSpinner;
+import java.util.Locale;
+
public class CommunicationDeviceView extends LinearLayout {
- private CheckBox mSpeakerphoneCheckbox;
- private TextView mIsSpeakerphoneText;
+
private AudioManager mAudioManager;
+ private CheckBox mSpeakerphoneCheckbox;
+ private CheckBox mScoCheckbox;
+ private TextView mSpeakerStatusView;
+ private TextView mScoStatusView;
+ private BroadcastReceiver mScoStateReceiver;
+ private boolean mScoStateReceiverRegistered = false;
private CommunicationDeviceSpinner mDeviceSpinner;
+ private int mScoState;
+ private CommDeviceSniffer mCommDeviceSniffer = new CommDeviceSniffer();;
+
+ protected class CommDeviceSniffer extends NativeSniffer {
+ @Override
+ public void updateStatusText() {
+ showCommDeviceStatus();
+ }
+ }
public CommunicationDeviceView(Context context) {
super(context);
@@ -66,6 +86,29 @@ public class CommunicationDeviceView extends LinearLayout {
mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
mSpeakerphoneCheckbox = (CheckBox) findViewById(R.id.setSpeakerphoneOn);
+ mSpeakerphoneCheckbox.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ onSetSpeakerphoneOn(view);
+ }
+ });
+ mSpeakerStatusView = (TextView) findViewById(R.id.spkr_status_view);
+
+ mScoCheckbox = (CheckBox) findViewById(R.id.setBluetoothScoOn);
+ mScoCheckbox.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ onStartStopBluetoothSco(view);
+ }
+ });
+ mScoStatusView = (TextView) findViewById(R.id.sco_status_view);
+ mScoStateReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ mScoState = intent.getIntExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, -1);
+ showCommDeviceStatus();
+ }
+ };
mDeviceSpinner = (CommunicationDeviceSpinner) findViewById(R.id.comm_devices_spinner);
mDeviceSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@@ -74,15 +117,15 @@ public class CommunicationDeviceView extends LinearLayout {
AudioDeviceInfo[] commDeviceArray = mDeviceSpinner.getCommunicationsDevices();
if (commDeviceArray != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
- if (position == 0) {
+ if (position == CommunicationDeviceSpinner.POS_CLEAR) {
mAudioManager.clearCommunicationDevice();
} else {
- AudioDeviceInfo selectedDevice = commDeviceArray[position - 1]; // skip "Clear"
+ AudioDeviceInfo selectedDevice = commDeviceArray[position - CommunicationDeviceSpinner.POS_DEVICES]; // skip "Clear"
mAudioManager.setCommunicationDevice(selectedDevice);
}
- showCommDeviceStatus();
}
}
+ showCommDeviceStatus();
}
public void onNothingSelected(AdapterView<?> parent) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
@@ -92,19 +135,22 @@ public class CommunicationDeviceView extends LinearLayout {
}
});
- mSpeakerphoneCheckbox.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- onSetSpeakerphoneOn(view);
- }
- });
- mIsSpeakerphoneText = (TextView) findViewById(R.id.isSpeakerphoneOn);
showCommDeviceStatus();
}
- public void cleanup() {
+ public void onStart() {
+ registerScoStateReceiver();
+ mCommDeviceSniffer.startSniffer();
+ }
+
+
+ public void onStop() {
+ mCommDeviceSniffer.stopSniffer();
mSpeakerphoneCheckbox.setChecked(false);
setSpeakerPhoneOn(false);
+ mScoCheckbox.setChecked(false);
+ mAudioManager.stopBluetoothSco();
+ unregisterScoStateReceiver();
}
public void onSetSpeakerphoneOn(View view) {
@@ -122,14 +168,47 @@ public class CommunicationDeviceView extends LinearLayout {
private void showCommDeviceStatus() {
boolean enabled = mAudioManager.isSpeakerphoneOn();
- String text = (enabled ? "ON" : "OFF");
+ String text = ":" + (enabled ? "ON" : "OFF");
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
AudioDeviceInfo commDeviceInfo = mAudioManager.getCommunicationDevice();
if (commDeviceInfo != null) {
- text += ", CommDev=" + commDeviceInfo.getId();
+ text += ", #" + commDeviceInfo.getId();
}
}
- mIsSpeakerphoneText.setText(" => " + text);
+ mSpeakerStatusView.setText(text + ",");
+
+ if (mScoState == AudioManager.SCO_AUDIO_STATE_CONNECTING) {
+ text = ":WAIT";
+ } else if (mScoState == AudioManager.SCO_AUDIO_STATE_CONNECTED) {
+ text = ":CON";
+ } else if (mScoState == AudioManager.SCO_AUDIO_STATE_DISCONNECTED) {
+ text = ":DISCON";
+ }
+ mScoStatusView.setText(text);
+ }
+
+ public void onStartStopBluetoothSco(View view) {
+ CheckBox checkBox = (CheckBox) view;
+ if (checkBox.isChecked()) {
+ mAudioManager.startBluetoothSco();
+ } else {
+ mAudioManager.stopBluetoothSco();
+ }
+ }
+
+ private synchronized void registerScoStateReceiver() {
+ if (!mScoStateReceiverRegistered) {
+ getContext().registerReceiver(mScoStateReceiver,
+ new IntentFilter(AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED));
+ mScoStateReceiverRegistered = true;
+ }
+ }
+
+ private synchronized void unregisterScoStateReceiver() {
+ if (mScoStateReceiverRegistered) {
+ getContext().unregisterReceiver(mScoStateReceiver);
+ mScoStateReceiverRegistered = false;
+ }
}
}
diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/DeviceReportActivity.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/DeviceReportActivity.java
index a9e8e606..31146236 100644
--- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/DeviceReportActivity.java
+++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/DeviceReportActivity.java
@@ -19,13 +19,22 @@ package com.mobileer.oboetester;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
+import android.content.Intent;
+import android.hardware.usb.UsbDevice;
+import android.hardware.usb.UsbManager;
import android.media.AudioDeviceCallback;
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
import android.media.MicrophoneInfo;
+import android.media.midi.MidiDeviceInfo;
+import android.media.midi.MidiManager;
import android.os.Build;
import android.os.Bundle;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
import android.widget.TextView;
+import android.widget.Toast;
import com.mobileer.audio_device.AudioDeviceInfoConverter;
@@ -33,10 +42,10 @@ import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
+import java.util.Set;
/**
- * Guide the user through a series of tests plugging in and unplugging a headset.
- * Print a summary at the end of any failures.
+ * Print a report of all the available audio devices.
*/
public class DeviceReportActivity extends Activity {
@@ -63,6 +72,8 @@ public class DeviceReportActivity extends Activity {
MyAudioDeviceCallback mDeviceCallback = new MyAudioDeviceCallback();
private TextView mAutoTextView;
private AudioManager mAudioManager;
+ private UsbManager mUsbManager;
+ private MidiManager mMidiManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -70,6 +81,26 @@ public class DeviceReportActivity extends Activity {
setContentView(R.layout.activity_device_report);
mAutoTextView = (TextView) findViewById(R.id.text_log_device_report);
mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
+ mUsbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
+ mMidiManager = (MidiManager) getSystemService(Context.MIDI_SERVICE);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.menu_main, menu);
+ MenuItem settings = menu.findItem(R.id.action_share);
+ settings.setOnMenuItemClickListener(item -> {
+ if(mAutoTextView !=null) {
+ Intent sendIntent = new Intent();
+ sendIntent.setAction(Intent.ACTION_SEND);
+ sendIntent.putExtra(Intent.EXTRA_TEXT, mAutoTextView.getText().toString());
+ sendIntent.setType("text/plain");
+ Intent shareIntent = Intent.createChooser(sendIntent, null);
+ startActivity(shareIntent);
+ }
+ return false;
+ });
+ return true;
}
@Override
@@ -112,9 +143,78 @@ public class DeviceReportActivity extends Activity {
report.append(item);
}
report.append(reportAllMicrophones());
+ report.append(reportUsbDevices());
+ report.append(reportMidiDevices());
log(report.toString());
}
+ public String reportUsbDevices() {
+ StringBuffer report = new StringBuffer();
+ report.append("\n############################");
+ report.append("\nUsb Device Report:\n");
+ try {
+ HashMap<String, UsbDevice> usbDeviceList = mUsbManager.getDeviceList();
+ for (UsbDevice usbDevice : usbDeviceList.values()) {
+ report.append("\n==== USB Device ========= " + usbDevice.getDeviceId());
+ report.append("\nProduct Name : " + usbDevice.getProductName());
+ report.append("\nProduct ID : 0x" + Integer.toHexString(usbDevice.getProductId()));
+ report.append("\nManufacturer Name : " + usbDevice.getManufacturerName());
+ report.append("\nVendor ID : 0x" + Integer.toHexString(usbDevice.getVendorId()));
+ report.append("\nDevice Name : " + usbDevice.getDeviceName());
+ report.append("\nDevice Protocol : " + usbDevice.getDeviceProtocol());
+ report.append("\nDevice Class : " + usbDevice.getDeviceClass());
+ report.append("\nDevice Subclass : " + usbDevice.getDeviceSubclass());
+ report.append("\nVersion : " + usbDevice.getVersion());
+ report.append("\n" + usbDevice);
+ report.append("\n");
+ }
+ } catch (Exception e) {
+ Log.e(TestAudioActivity.TAG, "Caught ", e);
+ showErrorToast(e.getMessage());
+ report.append("\nERROR: " + e.getMessage() + "\n");
+ }
+ return report.toString();
+ }
+
+ public String reportMidiDevices() {
+ StringBuffer report = new StringBuffer();
+ report.append("\n############################");
+ report.append("\nMidi Device Report:\n");
+ try {
+ MidiDeviceInfo[] midiDeviceInfos = mMidiManager.getDevices();
+ for (MidiDeviceInfo midiDeviceInfo : midiDeviceInfos) {
+ report.append("\n==== MIDI Device ========= " + midiDeviceInfo.getId());
+ addMidiDeviceInfoToDeviceReport(midiDeviceInfo, report);
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ Set<MidiDeviceInfo> umpDeviceInfos =
+ mMidiManager.getDevicesForTransport(MidiManager.TRANSPORT_UNIVERSAL_MIDI_PACKETS);
+ for (MidiDeviceInfo midiDeviceInfo : umpDeviceInfos) {
+ report.append("\n==== UMP Device ========= " + midiDeviceInfo.getId());
+ addMidiDeviceInfoToDeviceReport(midiDeviceInfo, report);
+ }
+ }
+ } catch (Exception e) {
+ Log.e(TestAudioActivity.TAG, "Caught ", e);
+ showErrorToast(e.getMessage());
+ report.append("\nERROR: " + e.getMessage() + "\n");
+ }
+ return report.toString();
+ }
+
+ private void addMidiDeviceInfoToDeviceReport(MidiDeviceInfo midiDeviceInfo,
+ StringBuffer report){
+ report.append("\nInput Count : " + midiDeviceInfo.getInputPortCount());
+ report.append("\nOutput Count : " + midiDeviceInfo.getOutputPortCount());
+ report.append("\nType : " + midiDeviceInfo.getType());
+ report.append("\nIs Private : " + midiDeviceInfo.isPrivate());
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ report.append("\nDefault Protocol : " + midiDeviceInfo.getDefaultProtocol());
+ }
+ report.append("\n" + midiDeviceInfo);
+ report.append("\n");
+ }
+
public String reportAllMicrophones() {
StringBuffer report = new StringBuffer();
report.append("\n############################");
@@ -127,8 +227,12 @@ public class DeviceReportActivity extends Activity {
report.append(micItem);
}
} catch (IOException e) {
- e.printStackTrace();
+ Log.e(TestAudioActivity.TAG, "Caught ", e);
return e.getMessage();
+ } catch (Exception e) {
+ Log.e(TestAudioActivity.TAG, "Caught ", e);
+ showErrorToast(e.getMessage());
+ report.append("\nERROR: " + e.getMessage() + "\n");
}
} else {
report.append("\nMicrophoneInfo not available on V" + android.os.Build.VERSION.SDK_INT);
@@ -169,4 +273,20 @@ public class DeviceReportActivity extends Activity {
});
}
+ protected void showErrorToast(String message) {
+ String text = "Error: " + message;
+ Log.e(TestAudioActivity.TAG, text);
+ showToast(text);
+ }
+
+ protected void showToast(final String message) {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ Toast.makeText(DeviceReportActivity.this,
+ message,
+ Toast.LENGTH_SHORT).show();
+ }
+ });
+ }
}
diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/DynamicWorkloadActivity.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/DynamicWorkloadActivity.java
new file mode 100644
index 00000000..8c94a3b7
--- /dev/null
+++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/DynamicWorkloadActivity.java
@@ -0,0 +1,388 @@
+/*
+ * Copyright 2015 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.mobileer.oboetester;
+
+import android.graphics.Color;
+import android.graphics.Typeface;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.view.View;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Locale;
+
+/**
+ * Demonstrate the behavior of a changing CPU load on underruns.
+ * Display the workload and the callback duration in a chart.
+ * Enable or disable PerformanceHints (ADPF) using a checkbox.
+ * This might boost the CPU frequency when Oboe is taking too long to compute the next buffer.
+ * ADPF docs at: https://developer.android.com/reference/android/os/PerformanceHintManager
+ */
+public class DynamicWorkloadActivity extends TestOutputActivityBase {
+ private static final int WORKLOAD_HIGH_MIN = 30;
+ private static final int WORKLOAD_HIGH_MAX = 150;
+ // When the CPU is completely saturated then the load will be above 1.0.
+ public static final double LOAD_RECOVERY_HIGH = 1.0;
+ // Use a slightly lower value for going low so that the comparator has hysteresis.
+ public static final double LOAD_RECOVERY_LOW = 0.95;
+
+ private static final float MARGIN_ABOVE_WORKLOAD_FOR_CPU = 1.2f;
+
+ // By default, set high workload to 70 voices, which is reasonable for most devices.
+ public static final double WORKLOAD_PROGRESS_FOR_70_VOICES = 0.53;
+
+ private Button mStopButton;
+ private Button mStartButton;
+ private TextView mResultView;
+ private LinearLayout mAffinityLayout;
+ private ArrayList<CheckBox> mAffinityBoxes = new ArrayList<CheckBox>();
+ private WorkloadUpdateThread mUpdateThread;
+
+ private MultiLineChart mMultiLineChart;
+ private MultiLineChart.Trace mMaxCpuLoadTrace;
+ private MultiLineChart.Trace mWorkloadTrace;
+ private CheckBox mUseAltAdpfBox;
+ private CheckBox mPerfHintBox;
+ private boolean mDrawChartAlways = true;
+ private CheckBox mDrawAlwaysBox;
+ private int mCpuCount;
+
+ private static final int WORKLOAD_LOW = 1;
+ private int mWorkloadHigh; // this will get set later
+ private WorkloadView mDynamicWorkloadView;
+
+ // Periodically query the status of the streams.
+ protected class WorkloadUpdateThread {
+ public static final int SNIFFER_UPDATE_PERIOD_MSEC = 40;
+ public static final int SNIFFER_UPDATE_DELAY_MSEC = 300;
+ public static final int SNIFFER_TOGGLE_PERIOD_MSEC = 3000;
+ private static final int STATE_IDLE = 0;
+ private static final int STATE_RUN_LOW = 1;
+ private static final int STATE_RUN_HIGH = 2;
+
+ private Handler mHandler;
+
+ private int mWorkloadCurrent = 1;
+
+ private int mState = STATE_IDLE;
+ private long mLastToggleTime = 0;
+ private long mRecoveryTimeBegin;
+ private long mRecoveryTimeEnd;
+ private long mStartTimeNanos;
+
+ String stateToString(int state) {
+ switch(state) {
+ case STATE_IDLE:
+ return "Idle";
+ case STATE_RUN_LOW:
+ return "low";
+ case STATE_RUN_HIGH:
+ return "HIGH";
+ default:
+ return "Unrecognized";
+ }
+ }
+
+ // Display status info for the stream.
+ private Runnable runnableCode = new Runnable() {
+ @Override
+ public void run() {
+ int nextWorkload = mWorkloadCurrent;
+ AudioStreamBase stream = mAudioOutTester.getCurrentAudioStream();
+ float cpuLoad = stream.getCpuLoad();
+ float maxCpuLoad = stream.getAndResetMaxCpuLoad();
+ int cpuMask = stream.getAndResetCpuMask();
+ long now = System.currentTimeMillis();
+ boolean drawChartOnce = false;
+
+ switch (mState) {
+ case STATE_IDLE:
+ drawChartOnce = true; // clear old chart
+ mState = STATE_RUN_LOW;
+ mLastToggleTime = now;
+ break;
+ case STATE_RUN_LOW:
+ nextWorkload = WORKLOAD_LOW;
+ if ((now - mLastToggleTime) > SNIFFER_TOGGLE_PERIOD_MSEC) {
+ mLastToggleTime = now;
+ mState = STATE_RUN_HIGH;
+ mRecoveryTimeBegin = 0;
+ mRecoveryTimeEnd = 0;
+ }
+ break;
+ case STATE_RUN_HIGH:
+ nextWorkload = mWorkloadHigh;
+ if ((now - mLastToggleTime) > SNIFFER_TOGGLE_PERIOD_MSEC) {
+ mLastToggleTime = now;
+ mState = STATE_RUN_LOW;
+ // Draw now when a CPU spike will not affect the result.
+ drawChartOnce = true;
+ }
+
+ if (mRecoveryTimeBegin == 0) {
+ if (maxCpuLoad > LOAD_RECOVERY_HIGH) {
+ mRecoveryTimeBegin = now;
+ }
+ } else if (mRecoveryTimeEnd == 0) {
+ if (maxCpuLoad < LOAD_RECOVERY_LOW) {
+ mRecoveryTimeEnd = now;
+ }
+ } else if (maxCpuLoad > LOAD_RECOVERY_LOW) {
+ mRecoveryTimeEnd = now;
+ }
+ break;
+ }
+ stream.setWorkload((int) nextWorkload);
+ mWorkloadCurrent = nextWorkload;
+ // Update chart
+ float nowMicros = (System.nanoTime() - mStartTimeNanos) * 0.001f;
+ mMultiLineChart.addX(nowMicros);
+ mMaxCpuLoadTrace.add((float) maxCpuLoad);
+ mWorkloadTrace.add((float) mWorkloadCurrent);
+ if (drawChartOnce || mDrawChartAlways){
+ mMultiLineChart.update();
+ }
+
+ // Display numbers
+ String recoveryTimeString = (mRecoveryTimeEnd <= mRecoveryTimeBegin) ?
+ "---" : ((mRecoveryTimeEnd - mRecoveryTimeBegin) + " msec");
+ String message =
+ "#Voices = " + (int) nextWorkload
+ + "\nWorkState = " + stateToString(mState)
+ + "\nCPU = " + String.format(Locale.getDefault(), "%6.3f%c", cpuLoad * 100, '%')
+ + "\ncores = " + cpuMaskToString(cpuMask, mCpuCount)
+ + "\nRecovery = " + recoveryTimeString;
+ postResult(message);
+
+ mHandler.postDelayed(runnableCode, SNIFFER_UPDATE_PERIOD_MSEC);
+ }
+ };
+
+ private void start() {
+ stop();
+ mStartTimeNanos = System.nanoTime();
+ mMultiLineChart.reset();
+ mState = STATE_IDLE;
+ mHandler = new Handler(Looper.getMainLooper());
+ // Start the initial runnable task by posting through the handler
+ mHandler.postDelayed(runnableCode, SNIFFER_UPDATE_DELAY_MSEC);
+ }
+
+ private void stop() {
+ if (mHandler != null) {
+ mHandler.removeCallbacks(runnableCode);
+ }
+ }
+
+ }
+
+ private void setWorkloadHigh(int workloadHigh) {
+ mWorkloadHigh = workloadHigh;
+ }
+
+
+ /**
+ * This text will look best in a monospace font.
+ * @param cpuMask CPU core bit mask
+ * @return a text display of the selected cores like "--2-45-7"
+ */
+ // TODO move this to some utility class
+ private String cpuMaskToString(int cpuMask, int cpuCount) {
+ String text = "";
+ long longMask = ((long) cpuMask) & 0x0FFFFFFFFL;
+ int index = 0;
+ while (longMask != 0 || index < cpuCount) {
+ text += ((longMask & 1) != 0) ? hexDigit(index) : "-";
+ longMask = longMask >> 1;
+ index++;
+ }
+ return text;
+ }
+
+ private char hexDigit(int n) {
+ byte x = (byte)(n & 0x0F);
+ if (x < 10) return (char)('0' + x);
+ else return (char)('A' + x);
+ }
+
+ @Override
+ protected void inflateActivity() {
+ setContentView(R.layout.activity_dynamic_workload);
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ mAudioOutTester = addAudioOutputTester();
+
+ mResultView = (TextView) findViewById(R.id.resultView);
+ mResultView.setTypeface(Typeface.MONOSPACE);
+ mStartButton = (Button) findViewById(R.id.button_start);
+ mStopButton = (Button) findViewById(R.id.button_stop);
+
+ mDynamicWorkloadView = (WorkloadView) findViewById(R.id.dynamic_workload_view);
+ mWorkloadView.setVisibility(View.GONE);
+
+ // Add a row of checkboxes for setting CPU affinity.
+ mCpuCount = NativeEngine.getCpuCount();
+ final int defaultCpuAffinityMask = 0;
+ View.OnClickListener checkBoxListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ // Create a mack from all the checkboxes.
+ int mask = 0;
+ for (int cpuIndex = 0; cpuIndex < mCpuCount; cpuIndex++) {
+ CheckBox checkBox = mAffinityBoxes.get(cpuIndex);
+ if (checkBox.isChecked()) {
+ mask |= (1 << cpuIndex);
+ }
+ }
+ NativeEngine.setCpuAffinityMask(mask);
+ }
+ };
+ mAffinityLayout = (LinearLayout) findViewById(R.id.affinityLayout);
+ for (int cpuIndex = 0; cpuIndex < mCpuCount; cpuIndex++) {
+ CheckBox checkBox = new CheckBox(DynamicWorkloadActivity.this);
+ mAffinityLayout.addView(checkBox);
+ mAffinityBoxes.add(checkBox);
+ checkBox.setText(cpuIndex + "");
+ checkBox.setOnClickListener(checkBoxListener);
+ if (((1 << cpuIndex) & defaultCpuAffinityMask) != 0) {
+ checkBox.setChecked(true);
+ }
+ }
+ NativeEngine.setCpuAffinityMask(defaultCpuAffinityMask);
+
+ mMultiLineChart = (MultiLineChart) findViewById(R.id.multiline_chart);
+ mMaxCpuLoadTrace = mMultiLineChart.createTrace("CPU", Color.RED,
+ 0.0f, 2.0f);
+ mWorkloadTrace = mMultiLineChart.createTrace("Work", Color.BLUE,
+ 0.0f, (MARGIN_ABOVE_WORKLOAD_FOR_CPU * WORKLOAD_HIGH_MAX));
+
+ mPerfHintBox = (CheckBox) findViewById(R.id.enable_perf_hint);
+
+ // TODO remove when finished with ADPF experiments.
+ mUseAltAdpfBox = (CheckBox) findViewById(R.id.use_alternative_adpf);
+ mUseAltAdpfBox.setOnClickListener(buttonView -> {
+ CheckBox checkBox = (CheckBox) buttonView;
+ setUseAlternativeAdpf(checkBox.isChecked());
+ mPerfHintBox.setEnabled(!checkBox.isChecked());
+ });
+ mUseAltAdpfBox.setVisibility(View.GONE);
+
+ mPerfHintBox.setOnClickListener(buttonView -> {
+ CheckBox checkBox = (CheckBox) buttonView;
+ setPerformanceHintEnabled(checkBox.isChecked());
+ mUseAltAdpfBox.setEnabled(!checkBox.isChecked());
+ });
+
+ CheckBox hearWorkloadBox = (CheckBox) findViewById(R.id.hear_workload);
+ hearWorkloadBox.setOnClickListener(buttonView -> {
+ CheckBox checkBox = (CheckBox) buttonView;
+ setHearWorkload(checkBox.isChecked());
+ });
+
+ mDrawAlwaysBox = (CheckBox) findViewById(R.id.draw_always);
+ mDrawAlwaysBox.setOnClickListener(buttonView -> {
+ CheckBox checkBox = (CheckBox) buttonView;
+ mDrawChartAlways = checkBox.isChecked();
+ });
+
+ if (mDynamicWorkloadView != null) {
+ mDynamicWorkloadView.setWorkloadReceiver((w) -> {
+ setWorkloadHigh(w);
+ });
+
+ mDynamicWorkloadView.setLabel("High Workload");
+ mDynamicWorkloadView.setRange(WORKLOAD_HIGH_MIN, WORKLOAD_HIGH_MAX);
+ mDynamicWorkloadView.setFaderNormalizedProgress(WORKLOAD_PROGRESS_FOR_70_VOICES);
+ }
+
+ updateButtons(false);
+ updateEnabledWidgets();
+ hideSettingsViews(); // make more room
+ }
+
+ private void setHearWorkload(boolean checked) {
+ mAudioOutTester.getCurrentAudioStream().setHearWorkload(checked);
+ }
+
+ private void setPerformanceHintEnabled(boolean checked) {
+ mAudioOutTester.getCurrentAudioStream().setPerformanceHintEnabled(checked);
+ }
+
+ private void updateButtons(boolean running) {
+ mStartButton.setEnabled(!running);
+ mStopButton.setEnabled(running);
+ mPerfHintBox.setEnabled(running);
+ }
+
+ private void postResult(final String text) {
+ runOnUiThread(new Runnable() {
+ public void run() {
+ mResultView.setText(text);
+ }
+ });
+ }
+
+ @Override
+ int getActivityType() {
+ return ACTIVITY_DYNAMIC_WORKLOAD;
+ }
+
+ public void startTest(View view) {
+ try {
+ openAudio();
+ } catch (IOException e) {
+ e.printStackTrace();
+ showErrorToast("Open audio failed!");
+ return;
+ }
+ try {
+ super.startAudio();
+ updateButtons(true);
+ postResult("Running test");
+ mUpdateThread = new WorkloadUpdateThread();
+ mUpdateThread.start();
+ } catch (IOException e) {
+ e.printStackTrace();
+ showErrorToast("Start audio failed! " + e.getMessage());
+ return;
+ }
+ }
+
+ public void stopTest(View view) {
+ onStopTest();
+ }
+
+ @Override
+ public void onStopTest() {
+ WorkloadUpdateThread updateThread = mUpdateThread;
+ if (updateThread != null) {
+ updateThread.stop();
+ }
+ updateButtons(false);
+ super.onStopTest();
+ }
+}
diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/EchoActivity.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/EchoActivity.java
index 1d8e4dcc..9ce258a5 100644
--- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/EchoActivity.java
+++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/EchoActivity.java
@@ -42,9 +42,8 @@ public class EchoActivity extends TestInputActivity {
private Button mStartButton;
private Button mStopButton;
private TextView mStatusTextView;
- private CommunicationDeviceView mCommunicationDeviceView;
- private ColdStartSniffer mNativeSniffer = new ColdStartSniffer(this);
+ private ColdStartSniffer mNativeSniffer = new ColdStartSniffer();
protected static final int MAX_DELAY_TIME_PROGRESS = 1000;
@@ -71,10 +70,6 @@ public class EchoActivity extends TestInputActivity {
private int mInputLatency;
private int mOutputLatency;
- public ColdStartSniffer(Activity activity) {
- super(activity);
- }
-
@Override
public void startSniffer() {
stableCallCount = 0;
@@ -83,16 +78,10 @@ public class EchoActivity extends TestInputActivity {
super.startSniffer();
}
- public void run() {
+ @Override
+ public boolean isComplete() {
mInputLatency = getColdStartInputMillis();
mOutputLatency = getColdStartOutputMillis();
- updateStatusText();
- if (!isComplete()) {
- reschedule();
- }
- }
-
- private boolean isComplete() {
if (mInputLatency > 0 && mOutputLatency > 0) {
stableCallCount++;
}
@@ -116,11 +105,6 @@ public class EchoActivity extends TestInputActivity {
}
@Override
- public String getShortReport() {
- return getCurrentStatusReport();
- }
-
- @Override
public void updateStatusText() {
String message = getCurrentStatusReport();
mStatusTextView.setText(message);
@@ -163,14 +147,6 @@ public class EchoActivity extends TestInputActivity {
hideSettingsViews();
}
- @Override
- protected void onStop() {
- if (mCommunicationDeviceView != null) {
- mCommunicationDeviceView.cleanup();
- }
- super.onStop();
- }
-
private void setDelayTimeByPosition(int progress) {
mDelayTime = mTaperDelayTime.linearToExponential(
((double)progress)/MAX_DELAY_TIME_PROGRESS);
diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/ExternalFileWriter.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/ExternalFileWriter.java
new file mode 100644
index 00000000..759690e4
--- /dev/null
+++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/ExternalFileWriter.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.mobileer.oboetester;
+
+import android.content.Context;
+import android.util.Log;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+
+public class ExternalFileWriter {
+ private static final String TAG = "OboeTester";
+ private Context mContext;
+
+ public ExternalFileWriter(Context context) {
+ mContext = context;
+ }
+
+ public File writeStringToExternalFile(String result, String fileName) throws IOException {
+ File dir = mContext.getExternalFilesDir(null);
+ File resultFile = new File(dir, fileName);
+ Log.d(TAG, "EXTFILE = " + resultFile.getAbsolutePath());
+ Writer writer = null;
+ try {
+ writer = new OutputStreamWriter(new FileOutputStream(resultFile));
+ writer.write(result);
+ } finally {
+ if (writer != null) {
+ try {
+ writer.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ return resultFile;
+ }
+}
diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/ExternalTapToToneActivity.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/ExternalTapToToneActivity.java
index 06cf26d2..c978da37 100644
--- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/ExternalTapToToneActivity.java
+++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/ExternalTapToToneActivity.java
@@ -6,6 +6,7 @@ import android.content.pm.PackageManager;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
+import android.view.WindowManager;
import android.widget.Button;
import android.widget.Toast;
@@ -58,6 +59,7 @@ public class ExternalTapToToneActivity extends Activity {
mTapToToneTester.resetLatency();
mTapToToneTester.start();
updateButtons(true);
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
} catch (IOException e) {
e.printStackTrace();
showErrorToast("Start audio failed! " + e.getMessage());
@@ -68,11 +70,13 @@ public class ExternalTapToToneActivity extends Activity {
public void stopTest(View view) {
mTapToToneTester.stop();
updateButtons(false);
+ getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
@Override
public void onStop() {
mTapToToneTester.stop();
+ getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
super.onStop();
}
diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/ExtraTestsActivity.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/ExtraTestsActivity.java
index 557ceb41..c744329b 100644
--- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/ExtraTestsActivity.java
+++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/ExtraTestsActivity.java
@@ -29,4 +29,15 @@ public class ExtraTestsActivity extends BaseOboeTesterActivity {
launchTestActivity(TestErrorCallbackActivity.class);
}
+ public void onLaunchRouteDuringCallbackTest(View view) {
+ launchTestThatDoesRecording(TestRouteDuringCallbackActivity.class);
+ }
+
+ public void onLaunchDynamicWorkloadTest(View view) {
+ launchTestActivity(DynamicWorkloadActivity.class);
+ }
+
+ public void onLaunchColdStartLatencyTest(View view) {
+ launchTestActivity(TestColdStartLatencyActivity.class);
+ }
}
diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/GlitchActivity.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/GlitchActivity.java
index ce0182c1..63abe5cf 100644
--- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/GlitchActivity.java
+++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/GlitchActivity.java
@@ -23,6 +23,7 @@ import android.widget.Button;
import android.widget.TextView;
import java.io.IOException;
+import java.util.Locale;
/**
* Activity to measure the number of glitches.
@@ -46,22 +47,23 @@ public class GlitchActivity extends AnalyzerActivity {
native int getStateFrameCount(int state);
native int getGlitchCount();
+
+ // Number of frames in last glitch.
+ native int getGlitchLength();
+ native double getPhase();
native double getSignalToNoiseDB();
native double getPeakAmplitude();
native double getSineAmplitude();
+ native int getSinePeriod();
- private GlitchSniffer mGlitchSniffer;
- private NativeSniffer mNativeSniffer = createNativeSniffer();
+ protected NativeSniffer mNativeSniffer = createNativeSniffer();
synchronized NativeSniffer createNativeSniffer() {
- if (mGlitchSniffer == null) {
- mGlitchSniffer = new GlitchSniffer(this);
- }
- return mGlitchSniffer;
+ return new GlitchSniffer();
}
// Note that these strings must match the enum result_code in LatencyAnalyzer.h
- String stateToString(int resultCode) {
+ static String stateToString(int resultCode) {
switch (resultCode) {
case STATE_IDLE:
return "IDLE";
@@ -80,6 +82,10 @@ public class GlitchActivity extends AnalyzerActivity {
}
}
+ static String magnitudeToString(double magnitude) {
+ return String.format(Locale.US, "%6.4f", magnitude);
+ }
+
// Periodically query for glitches from the native detector.
protected class GlitchSniffer extends NativeSniffer {
@@ -100,10 +106,6 @@ public class GlitchActivity extends AnalyzerActivity {
private double mPeakAmplitude;
private double mSineAmplitude;
- public GlitchSniffer(Activity activity) {
- super(activity);
- }
-
@Override
public void startSniffer() {
long now = System.currentTimeMillis();
@@ -119,7 +121,7 @@ public class GlitchActivity extends AnalyzerActivity {
super.startSniffer();
}
- public void run() {
+ private void gatherData() {
int state = getAnalyzerState();
mSignalToNoiseDB = getSignalToNoiseDB();
mPeakAmplitude = getPeakAmplitude();
@@ -163,8 +165,6 @@ public class GlitchActivity extends AnalyzerActivity {
mLastGlitchFrames = glitchFrames;
mLastLockedFrames = lockedFrames;
mLastResetCount = resetCount;
-
- reschedule();
}
private String getCurrentStatusReport() {
@@ -173,39 +173,46 @@ public class GlitchActivity extends AnalyzerActivity {
StringBuffer message = new StringBuffer();
message.append("state = " + stateToString(mPreviousState) + "\n");
- message.append(String.format("unlocked.frames = %d\n", mLastUnlockedFrames));
- message.append(String.format("locked.frames = %d\n", mLastLockedFrames));
- message.append(String.format("glitch.frames = %d\n", mLastGlitchFrames));
- message.append(String.format("reset.count = %d\n", mLastResetCount - mStartResetCount));
- message.append(String.format("peak.amplitude = %8.6f\n", mPeakAmplitude));
- message.append(String.format("sine.amplitude = %8.6f\n", mSineAmplitude));
+ message.append(String.format(Locale.getDefault(), "unlocked.frames = %d\n", mLastUnlockedFrames));
+ message.append(String.format(Locale.getDefault(), "locked.frames = %d\n", mLastLockedFrames));
+ message.append(String.format(Locale.getDefault(), "glitch.frames = %d\n", mLastGlitchFrames));
+ message.append(String.format(Locale.getDefault(), "reset.count = %d\n", mLastResetCount - mStartResetCount));
+ message.append(String.format(Locale.getDefault(), "peak.amplitude = %8.6f\n", mPeakAmplitude));
+ message.append(String.format(Locale.getDefault(), "sine.amplitude = %8.6f\n", mSineAmplitude));
if (mLastLockedFrames > 0) {
- message.append(String.format("signal.noise.ratio.db = %5.1f\n", mSignalToNoiseDB));
+ message.append(String.format(Locale.getDefault(), "signal.noise.ratio.db = %5.1f\n", mSignalToNoiseDB));
}
- message.append(String.format("time.total = %4.2f seconds\n", totalSeconds));
+ message.append(String.format(Locale.getDefault(), "time.total = %4.2f seconds\n", totalSeconds));
if (mLastLockedFrames > 0) {
- message.append(String.format("time.no.glitches = %4.2f\n", mSecondsWithoutGlitches));
- message.append(String.format("max.time.no.glitches = %4.2f\n",
+ message.append(String.format(Locale.getDefault(), "time.no.glitches = %4.2f\n", mSecondsWithoutGlitches));
+ message.append(String.format(Locale.getDefault(), "max.time.no.glitches = %4.2f\n",
mMaxSecondsWithoutGlitches));
- message.append(String.format("glitch.count = %d\n", mLastGlitchCount));
+ message.append(String.format(Locale.getDefault(), "glitch.length = %d\n", getGlitchLength()));
+ message.append(String.format(Locale.getDefault(), "glitch.count = %d\n", mLastGlitchCount));
}
return message.toString();
}
- @Override
public String getShortReport() {
- String resultText = "#glitches = " + getLastGlitchCount()
+ String resultText = "amplitude: peak = " + magnitudeToString(mPeakAmplitude)
+ + ", sine = " + magnitudeToString(mSineAmplitude) + "\n";
+ if (mPeakAmplitude < 0.01) {
+ resultText += "WARNING: volume is very low!\n";
+ }
+ resultText += "#glitches = " + getLastGlitchCount()
+ ", #resets = " + getLastResetCount()
+ ", max no glitch = " + getMaxSecondsWithNoGlitch() + " secs\n";
- resultText += String.format("SNR = %5.1f db", mSignalToNoiseDB);
+ resultText += String.format(Locale.getDefault(), "SNR = %5.1f db", mSignalToNoiseDB);
resultText += ", #locked = " + mLastLockedFrames;
return resultText;
}
@Override
public void updateStatusText() {
+ gatherData();
mLastGlitchReport = getCurrentStatusReport();
setAnalyzerText(mLastGlitchReport);
+ maybeDisplayWaveform();
}
public double getMaxSecondsWithNoGlitch() {
@@ -227,13 +234,16 @@ public class GlitchActivity extends AnalyzerActivity {
protected void onGlitchDetected() {
}
+ protected void maybeDisplayWaveform() {}
+
protected void setAnalyzerText(String s) {
mAnalyzerTextView.setText(s);
}
/**
* Set tolerance to deviations from expected value.
- * The normalized value will be converted in the native code.
+ * The normalized value will be scaled by the measured magnitude
+ * of the sine wave..
* @param tolerance normalized between 0.0 and 1.0
*/
public native void setTolerance(float tolerance);
@@ -256,6 +266,12 @@ public class GlitchActivity extends AnalyzerActivity {
return mOutputChannel;
}
+ /**
+ * Set the duration of a periodic forced glitch.
+ * @param frames or zero for no glitch
+ */
+ public native void setForcedGlitchDuration(int frames);
+
public native void setInputChannelNative(int channel);
public native void setOutputChannelNative(int channel);
@@ -332,18 +348,10 @@ public class GlitchActivity extends AnalyzerActivity {
onTestBegan();
}
- public void onCancel(View view) {
- stopAudioTest();
- onTestFinished();
- }
-
// Called on UI thread
public void onStopAudioTest(View view) {
stopAudioTest();
onTestFinished();
- mStartButton.setEnabled(true);
- mStopButton.setEnabled(false);
- mShareButton.setEnabled(false);
keepScreenOn(false);
}
@@ -365,6 +373,7 @@ public class GlitchActivity extends AnalyzerActivity {
}
public void stopTest() {
+ mNativeSniffer.stopSniffer();
stopAudio();
}
@@ -374,11 +383,11 @@ public class GlitchActivity extends AnalyzerActivity {
}
public double getMaxSecondsWithNoGlitch() {
- return mGlitchSniffer.getMaxSecondsWithNoGlitch();
+ return ((GlitchSniffer)mNativeSniffer).getMaxSecondsWithNoGlitch();
}
public String getShortReport() {
- return mNativeSniffer.getShortReport();
+ return ((GlitchSniffer)mNativeSniffer).getShortReport();
}
@Override
diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/IntentBasedTestSupport.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/IntentBasedTestSupport.java
index 594e7c41..4746ec72 100644
--- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/IntentBasedTestSupport.java
+++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/IntentBasedTestSupport.java
@@ -39,13 +39,20 @@ public class IntentBasedTestSupport {
public static final String KEY_IN_USE_MMAP = "in_use_mmap";
public static final String KEY_OUT_USE_MMAP = "out_use_mmap";
- public static final boolean VALUE_DEFAULT_USE_MMAP = true;
+ public static final boolean VALUE_DEFAULT_USE_MMAP = NativeEngine.isMMapSupported();
public static final String KEY_IN_PRESET = "in_preset";
public static final String KEY_SAMPLE_RATE = "sample_rate";
public static final int VALUE_DEFAULT_SAMPLE_RATE = 48000;
public static final String VALUE_UNSPECIFIED = "unspecified";
+ public static final String KEY_OUT_USAGE = "out_usage";
+ public static final String VALUE_USAGE_MEDIA = "media";
+ public static final String VALUE_USAGE_VOICE_COMMUNICATION = "voice_communication";
+ public static final String VALUE_USAGE_ALARM = "alarm";
+ public static final String VALUE_USAGE_NOTIFICATION = "notification";
+ public static final String VALUE_USAGE_GAME = "game";
+
public static final String KEY_IN_API = "in_api";
public static final String KEY_OUT_API = "out_api";
public static final String VALUE_API_AAUDIO = "aaudio";
@@ -138,6 +145,21 @@ public class IntentBasedTestSupport {
return StreamConfiguration.SHARING_MODE_EXCLUSIVE;
}
}
+ public static int getUsageFromText(String text) {
+ if (VALUE_USAGE_GAME.equals(text)) {
+ return StreamConfiguration.USAGE_GAME;
+ } else if (VALUE_USAGE_VOICE_COMMUNICATION.equals(text)) {
+ return StreamConfiguration.USAGE_VOICE_COMMUNICATION;
+ } else if (VALUE_USAGE_MEDIA.equals(text)) {
+ return StreamConfiguration.USAGE_MEDIA;
+ } else if (VALUE_USAGE_ALARM.equals(text)) {
+ return StreamConfiguration.USAGE_ALARM;
+ } else if (VALUE_USAGE_NOTIFICATION.equals(text)) {
+ return StreamConfiguration.USAGE_NOTIFICATION;
+ } else {
+ return StreamConfiguration.UNSPECIFIED;
+ }
+ }
public static void configureStreamsFromBundle(Bundle bundle,
StreamConfiguration requestedInConfig,
@@ -278,6 +300,11 @@ public class IntentBasedTestSupport {
int sharingMode = getSharingFromText(text);
requestedOutConfig.setSharingMode(sharingMode);
+ text = bundle.getString(KEY_OUT_USAGE, VALUE_USAGE_MEDIA);
+ int usage = getUsageFromText(text);
+ requestedOutConfig.setUsage(usage);
+
+
}
public static void configureInputStreamFromBundle(Bundle bundle,
diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/MainActivity.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/MainActivity.java
index 70eca907..145dfbc0 100644
--- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/MainActivity.java
+++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/MainActivity.java
@@ -57,9 +57,7 @@ public class MainActivity extends BaseOboeTesterActivity {
protected TextView mDeviceView;
private TextView mVersionTextView;
private TextView mBuildTextView;
- private TextView mBluetoothScoStatusView;
private Bundle mBundleFromIntent;
- private BroadcastReceiver mScoStateReceiver;
private CheckBox mWorkaroundsCheckBox;
private CheckBox mBackgroundCheckBox;
private static String mVersionText;
@@ -117,21 +115,6 @@ public class MainActivity extends BaseOboeTesterActivity {
mBuildTextView = (TextView) findViewById(R.id.text_build_info);
mBuildTextView.setText(Build.DISPLAY);
- mBluetoothScoStatusView = (TextView) findViewById(R.id.textBluetoothScoStatus);
- mScoStateReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- int state = intent.getIntExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, -1);
- if (state == AudioManager.SCO_AUDIO_STATE_CONNECTING) {
- mBluetoothScoStatusView.setText("CONNECTING");
- } else if (state == AudioManager.SCO_AUDIO_STATE_CONNECTED) {
- mBluetoothScoStatusView.setText("CONNECTED");
- } else if (state == AudioManager.SCO_AUDIO_STATE_DISCONNECTED) {
- mBluetoothScoStatusView.setText("DISCONNECTED");
- }
- }
- };
-
saveIntentBundleForLaterProcessing(getIntent());
}
@@ -139,15 +122,6 @@ public class MainActivity extends BaseOboeTesterActivity {
return mVersionText;
}
- private void registerScoStateReceiver() {
- registerReceiver(mScoStateReceiver,
- new IntentFilter(AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED));
- }
-
- private void unregisterScoStateReceiver() {
- unregisterReceiver(mScoStateReceiver);
- }
-
private void logScreenSize() {
Display display = getWindowManager().getDefaultDisplay();
Point size = new Point();
@@ -213,16 +187,9 @@ public class MainActivity extends BaseOboeTesterActivity {
public void onResume(){
super.onResume();
mWorkaroundsCheckBox.setChecked(NativeEngine.areWorkaroundsEnabled());
- registerScoStateReceiver();
processBundleFromIntent();
}
- @Override
- public void onPause(){
- unregisterScoStateReceiver();
- super.onPause();
- }
-
private void updateNativeAudioUI() {
AudioManager myAudioMgr = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
String audioManagerSampleRate = myAudioMgr.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE);
@@ -312,13 +279,4 @@ public class MainActivity extends BaseOboeTesterActivity {
OboeAudioStream.setCallbackSize(callbackSize);
}
- public void onStartStopBluetoothSco(View view) {
- CheckBox checkBox = (CheckBox) view;
- AudioManager myAudioMgr = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
- if (checkBox.isChecked()) {
- myAudioMgr.startBluetoothSco();
- } else {
- myAudioMgr.stopBluetoothSco();
- }
- }
}
diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/ManualGlitchActivity.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/ManualGlitchActivity.java
index 699344df..9281cd59 100644
--- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/ManualGlitchActivity.java
+++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/ManualGlitchActivity.java
@@ -16,13 +16,21 @@
package com.mobileer.oboetester;
+import android.content.Context;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
+import android.util.Log;
+import android.view.View;
+import android.widget.CheckBox;
+import android.widget.LinearLayout;
+import android.widget.RadioButton;
+import android.widget.RadioGroup;
import android.widget.SeekBar;
import android.widget.TextView;
import java.io.IOException;
+import java.util.Locale;
public class ManualGlitchActivity extends GlitchActivity {
@@ -30,15 +38,24 @@ public class ManualGlitchActivity extends GlitchActivity {
public static final int VALUE_DEFAULT_BUFFER_BURSTS = 2;
public static final String KEY_TOLERANCE = "tolerance";
- private static final float DEFAULT_TOLERANCE = 0.1f;
+ private static final float DEFAULT_TOLERANCE = 0.10f;
private static final long MIN_DISPLAY_PERIOD_MILLIS = 500;
+ private static final int WAVEFORM_SIZE = 400;
private TextView mTextTolerance;
private SeekBar mFaderTolerance;
protected ExponentialTaper mTaperTolerance;
+
+ private CheckBox mForceGlitchesBox;
+ private CheckBox mAutoScopeBox;
private WaveformView mWaveformView;
- private float[] mWaveform = new float[256];
+ private LinearLayout mLayoutGlitch;
+
+
+ private NumberedRadioButtons mInputChannelBoxes;
+ private NumberedRadioButtons mOutputChannelBoxes;
+ private float[] mWaveform = new float[WAVEFORM_SIZE];
private long mLastDisplayTime;
private float mTolerance = DEFAULT_TOLERANCE;
@@ -62,7 +79,56 @@ public class ManualGlitchActivity extends GlitchActivity {
float tolerance = (float) mTaperTolerance.linearToExponential(
((double)progress) / FADER_PROGRESS_MAX);
setTolerance(tolerance);
- mTextTolerance.setText("Tolerance = " + String.format("%5.3f", tolerance));
+ mTextTolerance.setText("Tolerance = " + String.format(Locale.getDefault(), "%5.3f", tolerance));
+ }
+
+ static class NumberedRadioButtons {
+ LinearLayout mRow;
+ RadioButton[] mRadioButtons;
+
+ public interface SelectionListener {
+ void onSelected(int index);
+ }
+
+ NumberedRadioButtons(Context context, int numBoxes, SelectionListener listener, String prompt) {
+ mRow = new LinearLayout(context);
+ mRow.setOrientation(LinearLayout.HORIZONTAL);
+ TextView textView = new TextView(context);
+ textView.setText(prompt);
+ mRow.addView(textView);
+ RadioGroup rg = new RadioGroup(context);
+ rg.setOrientation(LinearLayout.HORIZONTAL);
+ mRadioButtons = new RadioButton[numBoxes];
+ for (int i = 0; i < numBoxes; i++) {
+ mRadioButtons[i] = new RadioButton(context);
+ mRadioButtons[i].setText("" + i);
+ mRadioButtons[i].setId(i);
+ rg.addView(mRadioButtons[i]);
+ }
+ mRow.addView(rg);
+
+ //set listener to radio button group
+ rg.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(RadioGroup group, int checkedId) {
+ listener.onSelected(checkedId);
+ }
+ });
+
+ mRadioButtons[0].setChecked(true);
+ }
+
+ public View getView() {
+ return mRow;
+ }
+
+ public void setMaxEnabled(int max) {
+ max = Math.min(max, mRadioButtons.length);
+ for (int i = 0; i < mRadioButtons.length; i++) {
+ mRadioButtons[i].setEnabled(i < max);
+ }
+ mRadioButtons[0].setChecked(true);
+ }
}
@Override
@@ -75,7 +141,17 @@ public class ManualGlitchActivity extends GlitchActivity {
mFaderTolerance.setOnSeekBarChangeListener(mToleranceListener);
setToleranceFader(DEFAULT_TOLERANCE);
+ mForceGlitchesBox = (CheckBox) findViewById(R.id.boxForceGlitch);
+ mAutoScopeBox = (CheckBox) findViewById(R.id.boxAutoScope);
mWaveformView = (WaveformView) findViewById(R.id.waveview_audio);
+
+ mLayoutGlitch = (LinearLayout) findViewById(R.id.layoutGlitch);
+ mInputChannelBoxes = new NumberedRadioButtons(this, 8,
+ (int index) -> setInputChannel(index), "IN:");
+ mLayoutGlitch.addView(mInputChannelBoxes.getView());
+ mOutputChannelBoxes = new NumberedRadioButtons(this, 8,
+ (int index) -> setOutputChannel(index), "OUT:");
+ mLayoutGlitch.addView(mOutputChannelBoxes.getView());
}
private void setToleranceFader(float tolerance) {
@@ -112,7 +188,12 @@ public class ManualGlitchActivity extends GlitchActivity {
public void startAudioTest() throws IOException {
super.startAudioTest();
+
setToleranceProgress(mFaderTolerance.getProgress());
+ int inChannels = mAudioInputTester.getCurrentAudioStream().getChannelCount();
+ mInputChannelBoxes.setMaxEnabled(inChannels);
+ int outChannels = mAudioOutTester.getCurrentAudioStream().getChannelCount();
+ mOutputChannelBoxes.setMaxEnabled(outChannels);
}
@Override
@@ -147,7 +228,7 @@ public class ManualGlitchActivity extends GlitchActivity {
void stopAutomaticTest() {
String report = getCommonTestReport()
- + String.format("tolerance = %5.3f\n", mTolerance)
+ + String.format(Locale.getDefault(), "tolerance = %5.3f\n", mTolerance)
+ mLastGlitchReport;
onStopAudioTest(null);
maybeWriteTestResult(report);
@@ -156,13 +237,8 @@ public class ManualGlitchActivity extends GlitchActivity {
// Only call from UI thread.
@Override
- public void onTestFinished() {
- super.onTestFinished();
- }
-
- // Only call from UI thread.
- @Override
public void onTestBegan() {
+ mAutoScopeBox.setChecked(true);
mWaveformView.clearSampleData();
mWaveformView.postInvalidate();
super.onTestBegan();
@@ -171,19 +247,45 @@ public class ManualGlitchActivity extends GlitchActivity {
// Called on UI thread
@Override
protected void onGlitchDetected() {
+ if (mAutoScopeBox.isChecked()) {
+ mAutoScopeBox.setChecked(false); // stop auto drawing of waveform
+ mLastDisplayTime = 0; // force draw first glitch
+ }
long now = System.currentTimeMillis();
+ Log.i(TAG,"onGlitchDetected: glitch");
if ((now - mLastDisplayTime) > MIN_DISPLAY_PERIOD_MILLIS) {
mLastDisplayTime = now;
int numSamples = getGlitch(mWaveform);
mWaveformView.setSampleData(mWaveform, 0, numSamples);
+ int glitchLength = getGlitchLength();
+ int[] cursors = new int[glitchLength > 0 ? 2 : 1];
+ int startOfGlitch = getSinePeriod();
+ cursors[0] = startOfGlitch;
+ if (glitchLength > 0) {
+ cursors[1] = startOfGlitch + getGlitchLength();
+ }
+ mWaveformView.setCursorData(cursors);
+ Log.i(TAG,"onGlitchDetected: glitch, numSamples = " + numSamples);
mWaveformView.postInvalidate();
}
}
-
- private float[] getGlitchWaveform() {
- return mWaveform;
+ @Override
+ protected void maybeDisplayWaveform() {
+ if (!mAutoScopeBox.isChecked()) return;
+ long now = System.currentTimeMillis();
+ if ((now - mLastDisplayTime) > MIN_DISPLAY_PERIOD_MILLIS) {
+ mLastDisplayTime = now;
+ int numSamples = getRecentSamples(mWaveform);
+ mWaveformView.setSampleData(mWaveform, 0, numSamples);
+ mWaveformView.setCursorData(null);
+ mWaveformView.postInvalidate();
+ }
}
private native int getGlitch(float[] mWaveform);
+ private native int getRecentSamples(float[] mWaveform);
+ public void onForceGlitchClicked(View view) {
+ setForcedGlitchDuration(mForceGlitchesBox.isChecked() ? 100 : 0);
+ }
}
diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/MicrophoneInfoConverter.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/MicrophoneInfoConverter.java
index 8dbe502b..e96b4cb3 100644
--- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/MicrophoneInfoConverter.java
+++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/MicrophoneInfoConverter.java
@@ -4,6 +4,7 @@ import android.media.MicrophoneInfo;
import android.util.Pair;
import java.util.List;
+import java.util.Locale;
public class MicrophoneInfoConverter {
@@ -39,7 +40,7 @@ public class MicrophoneInfoConverter {
static String convertCoordinates(MicrophoneInfo.Coordinate3F coordinates) {
if (coordinates == MicrophoneInfo.POSITION_UNKNOWN) return "Unknown";
- return String.format("{ %6.4g, %5.3g, %5.3g }",
+ return String.format(Locale.getDefault(), "{ %6.4g, %5.3g, %5.3g }",
coordinates.x, coordinates.y, coordinates.z);
}
@@ -60,6 +61,9 @@ public class MicrophoneInfoConverter {
sb.append("\nType : " + micInfo.getType());
List<Pair<Integer, Integer>> mapping = micInfo.getChannelMapping();
+ if (mapping == null) {
+ throw new RuntimeException("MicrophoneInfo. getChannelMapping() returned null!");
+ }
sb.append("\nChannelMapping: {");
for (Pair<Integer, Integer> pair : mapping) {
sb.append("[" + pair.first + "," + pair.second + "], ");
diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/MultiLineChart.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/MultiLineChart.java
new file mode 100644
index 00000000..9b21e985
--- /dev/null
+++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/MultiLineChart.java
@@ -0,0 +1,254 @@
+package com.mobileer.oboetester;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.util.AttributeSet;
+import android.view.View;
+
+import java.util.ArrayList;
+
+/**
+ * Draw a chart with multiple traces
+ */
+public class MultiLineChart extends View {
+ public static final int NUM_DATA_VALUES = 512;
+ private Paint mWavePaint;
+ private Paint mCursorPaint;
+ private int mBackgroundColor = 0xFFF0F0F0;
+ private int mLineColor = Color.RED;
+ private Paint mBackgroundPaint;
+ float[] mVertices = new float[4];
+
+ CircularFloatArray mXData = new CircularFloatArray(NUM_DATA_VALUES);
+ private ArrayList<Trace> mTraceList = new ArrayList<>();
+
+ public MultiLineChart(Context context) {
+ super(context);
+ init(null, 0);
+ }
+
+ public MultiLineChart(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init(attrs, 0);
+ }
+
+ public MultiLineChart(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ init(attrs, defStyle);
+ }
+
+ private void init(AttributeSet attrs, int defStyle) {
+ // Load attributes
+ final TypedArray a = getContext().obtainStyledAttributes(
+ attrs, R.styleable.MultiLineChart, defStyle, 0);
+
+ mBackgroundColor = a.getColor(
+ R.styleable.MultiLineChart_backgroundColor,
+ mBackgroundColor);
+ mLineColor = a.getColor(
+ R.styleable.MultiLineChart_backgroundColor,
+ mLineColor);
+
+ a.recycle();
+
+ mWavePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ mWavePaint.setColor(mLineColor);
+
+ mCursorPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ mCursorPaint.setColor(Color.RED);
+ mCursorPaint.setStrokeWidth(3.0f);
+
+ mBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ mBackgroundPaint.setColor(mBackgroundColor);
+ mBackgroundPaint.setStyle(Paint.Style.FILL);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ canvas.drawRect(0.0f, 0.0f, getWidth(),
+ getHeight(), mBackgroundPaint);
+
+ for (Trace trace : mTraceList) {
+ drawTrace(canvas, trace);
+ }
+ }
+
+ void drawTrace(Canvas canvas, Trace trace) {
+ // Determine bounds and XY conversion.
+ int numPoints = mXData.size();
+ if (numPoints < 2) return;
+ // Allocate array for polyline.
+ int arraySize = (numPoints - 1) * 4;
+ if (arraySize > mVertices.length) {
+ mVertices = new float[arraySize];
+ }
+ // Setup scaling.
+ float previousX = 0.0f;
+ float previousY = 0.0f;
+ float xMax = getXData(1);
+ float xRange = xMax - getXData(numPoints);
+ float yMin = trace.getMin();
+ float yRange = trace.getMax() - yMin;
+ float width = getWidth();
+ float height = getHeight();
+ float xScaler = width / xRange;
+ float yScaler = height / yRange;
+ // Iterate through the available data.
+ int vertexIndex = 0;
+ for (int i = 1; i < numPoints; i++) {
+ float xData = getXData(i);
+ float yData = trace.get(i);
+ float xPos = width - ((xMax - xData) * xScaler);
+ float yPos = height - ((yData - yMin) * yScaler);
+ if (i > 1) {
+ // Each line segment requires 4 values!
+ mVertices[vertexIndex++] = previousX;
+ mVertices[vertexIndex++] = previousY;
+ mVertices[vertexIndex++] = xPos;
+ mVertices[vertexIndex++] = yPos;
+ }
+ previousX = xPos;
+ previousY = yPos;
+ }
+ canvas.drawLines(mVertices, 0, vertexIndex, trace.paint);
+ }
+
+ public float getXData(int i) {
+ return mXData.get(i);
+ }
+
+ public MultiLineChart.Trace createTrace(String name, int color, float min, float max) {
+ Trace trace = new Trace(name, color, NUM_DATA_VALUES, min, max);
+ mTraceList.add(trace);
+ return trace;
+ }
+
+ public void update() {
+ post(new Runnable() {
+ public void run() {
+ postInvalidate();
+ }
+ });
+ }
+
+ public void addX(float value) {
+ mXData.add(value);
+ }
+
+ public void reset() {
+ mXData.clear();
+ for (Trace trace : mTraceList) {
+ trace.reset();
+ }
+ }
+
+ public static class Trace {
+ private final String mName;
+ public Paint paint;
+ protected float mMin;
+ protected float mMax;
+ protected CircularFloatArray mData;
+
+ private Trace(String name, int color, int numValues, float min, float max) {
+ mName = name;
+ mMin = min;
+ mMax = max;
+ mData = new CircularFloatArray(numValues);
+
+ paint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ paint.setColor(color);
+ paint.setStrokeWidth(3.0f);
+ }
+
+ public void reset() {
+ mData.clear();
+ }
+
+ public void add(float value) {
+ mData.add(value);
+ // Take the hit here instead of when drawing.
+ mData.add(Math.min(mMax, Math.max(mMin, value)));
+ }
+
+ public int size() {
+ return mData.size();
+ }
+
+ /**
+ * Fetch a previous value. A delayIndex of 1 will return the most recently written value.
+ * A delayIndex of 2 will return the previously written value;
+ * @param delayIndex positive index of previously written data
+ * @return old value
+ */
+ public float get(int delayIndex) {
+ return mData.get(delayIndex);
+ }
+ public float getMax() {
+ return mMax;
+ }
+ public float getMin() {
+ return mMin;
+ }
+
+ public void setMin(float min) {
+ mMin = min;
+ }
+ public void setMax(float max) {
+ mMax = max;
+ }
+ }
+
+ // Use explicit type for performance reasons.
+ private static class CircularFloatArray {
+ private float[] mData;
+ private int mIndexMask;
+ private int mCursor; // next location to be written
+
+ public CircularFloatArray(int numValuesPowerOf2) {
+ if ((numValuesPowerOf2 & (numValuesPowerOf2 - 1)) != 0) {
+ throw new IllegalArgumentException("numValuesPowerOf2 not 2^N, was " + numValuesPowerOf2);
+ }
+ mData = new float[numValuesPowerOf2];
+ mIndexMask = numValuesPowerOf2 - 1;
+ }
+
+ /**
+ * Add one value to the array.
+ * This may overwrite the oldest data.
+ * @param value
+ */
+ public void add(float value) {
+ int index = mCursor & mIndexMask;
+ mData[index] = value;
+ mCursor++;
+ }
+
+ /**
+ * Number of valid entries.
+ * @return
+ */
+ public int size() {
+ return Math.min(mCursor, mData.length);
+ }
+
+ /**
+ * Fetch a previous value. A delayIndex of 1 will return the most recently written value.
+ * A delayIndex of 2 will return the previously written value;
+ * @param delayIndex positive index of previously written data
+ * @return old value
+ */
+ public float get(int delayIndex) {
+ int index = (mCursor - delayIndex) & mIndexMask;
+ return mData[index];
+ }
+
+ public void clear() {
+ mCursor = 0;
+ }
+ }
+} \ No newline at end of file
diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/NativeEngine.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/NativeEngine.java
index 6978bdca..985797b1 100644
--- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/NativeEngine.java
+++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/NativeEngine.java
@@ -9,4 +9,8 @@ public class NativeEngine {
static native void setWorkaroundsEnabled(boolean enabled);
static native boolean areWorkaroundsEnabled();
+
+ static native int getCpuCount();
+
+ static native void setCpuAffinityMask(int mask);
}
diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/NativeSniffer.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/NativeSniffer.java
index 1bc1232c..222173d7 100644
--- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/NativeSniffer.java
+++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/NativeSniffer.java
@@ -16,23 +16,24 @@
package com.mobileer.oboetester;
-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;
+ @Override
+ public void run() {
+ if (mEnabled && !isComplete()) {
+ updateStatusText();
+ mHandler.postDelayed(this, SNIFFER_UPDATE_PERIOD_MSEC);
+ }
}
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);
@@ -42,28 +43,19 @@ abstract class NativeSniffer implements Runnable {
mEnabled = false;
if (mHandler != null) {
mHandler.removeCallbacks(this);
+ // Final update of the text.
+ mHandler.post(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);
- }
+ /**
+ * You can override this is if you want to control when sniffing is finished.
+ * @return true if finished
+ */
+ public boolean isComplete() {
+ return false;
}
public abstract void updateStatusText();
- public String getShortReport() {
- return "no-report";
- }
-
}
diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/OboeAudioOutputStream.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/OboeAudioOutputStream.java
index 92cde485..7a727db4 100644
--- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/OboeAudioOutputStream.java
+++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/OboeAudioOutputStream.java
@@ -31,4 +31,6 @@ public class OboeAudioOutputStream extends OboeAudioStream {
public native void setChannelEnabled(int channelIndex, boolean enabled);
public native void setSignalType(int type);
+
+ public native void setAmplitude(float amplitude);
}
diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/OboeAudioStream.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/OboeAudioStream.java
index 5ff2dd12..5c8b2109 100644
--- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/OboeAudioStream.java
+++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/OboeAudioStream.java
@@ -29,7 +29,7 @@ abstract class OboeAudioStream extends AudioStreamBase {
@Override
public void stopPlayback() throws IOException {
int result = stopPlaybackNative();
- if (result < 0) {
+ if (result != 0) {
throw new IOException("Stop Playback failed! result = " + result);
}
}
@@ -39,19 +39,13 @@ abstract class OboeAudioStream extends AudioStreamBase {
@Override
public void startPlayback() throws IOException {
int result = startPlaybackNative();
- if (result < 0) {
+ if (result != 0) {
throw new IOException("Start Playback failed! result = " + result);
}
}
public native int startPlaybackNative();
- // Write disabled because the synth is in native code.
- @Override
- public int write(float[] buffer, int offset, int length) {
- return 0;
- }
-
@Override
public void open(StreamConfiguration requestedConfiguration,
StreamConfiguration actualConfiguration, int bufferSizeInFrames) throws IOException {
@@ -66,6 +60,7 @@ abstract class OboeAudioStream extends AudioStreamBase {
requestedConfiguration.getInputPreset(),
requestedConfiguration.getUsage(),
requestedConfiguration.getContentType(),
+ requestedConfiguration.getBufferCapacityInFrames(),
requestedConfiguration.getDeviceId(),
requestedConfiguration.getSessionId(),
requestedConfiguration.getChannelConversionAllowed(),
@@ -76,7 +71,11 @@ abstract class OboeAudioStream extends AudioStreamBase {
);
if (result < 0) {
streamIndex = INVALID_STREAM_INDEX;
- throw new IOException("Open failed! result = " + result);
+ String message = "Open "
+ + (isInput() ? "Input" : "Output")
+ + " failed! result = " + result + ", "
+ + StreamConfiguration.convertErrorToText(result);
+ throw new IOException(message);
} else {
streamIndex = result;
}
@@ -98,6 +97,9 @@ abstract class OboeAudioStream extends AudioStreamBase {
actualConfiguration.setDirection(isInput()
? StreamConfiguration.DIRECTION_INPUT
: StreamConfiguration.DIRECTION_OUTPUT);
+ actualConfiguration.setHardwareChannelCount(getHardwareChannelCount());
+ actualConfiguration.setHardwareSampleRate(getHardwareSampleRate());
+ actualConfiguration.setHardwareFormat(getHardwareFormat());
}
private native int openNative(
@@ -111,6 +113,7 @@ abstract class OboeAudioStream extends AudioStreamBase {
int inputPreset,
int usage,
int contentType,
+ int bufferCapacityInFrames,
int deviceId,
int sessionId,
boolean channelConversionAllowed,
@@ -141,16 +144,23 @@ abstract class OboeAudioStream extends AudioStreamBase {
private native int getBufferSizeInFrames(int streamIndex);
@Override
- public boolean isThresholdSupported() {
- return true;
- }
-
- @Override
public int setBufferSizeInFrames(int thresholdFrames) {
return setBufferSizeInFrames(streamIndex, thresholdFrames);
}
private native int setBufferSizeInFrames(int streamIndex, int thresholdFrames);
+ @Override
+ public void setPerformanceHintEnabled(boolean checked) {
+ setPerformanceHintEnabled(streamIndex, checked);
+ }
+ private native void setPerformanceHintEnabled(int streamIndex, boolean checked);
+
+ @Override
+ public void setHearWorkload(boolean checked) {
+ setHearWorkload(streamIndex, checked);
+ }
+ private native void setHearWorkload(int streamIndex, boolean checked);
+
public int getNativeApi() {
return getNativeApi(streamIndex);
}
@@ -207,6 +217,21 @@ abstract class OboeAudioStream extends AudioStreamBase {
}
private native int getChannelMask(int streamIndex);
+ public int getHardwareChannelCount() {
+ return getHardwareChannelCount(streamIndex);
+ }
+ private native int getHardwareChannelCount(int streamIndex);
+
+ public int getHardwareSampleRate() {
+ return getHardwareSampleRate(streamIndex);
+ }
+ private native int getHardwareSampleRate(int streamIndex);
+
+ public int getHardwareFormat() {
+ return getHardwareFormat(streamIndex);
+ }
+ private native int getHardwareFormat(int streamIndex);
+
public int getDeviceId() {
return getDeviceId(streamIndex);
}
@@ -217,6 +242,7 @@ abstract class OboeAudioStream extends AudioStreamBase {
}
private native int getSessionId(int streamIndex);
+
public boolean isMMap() {
return isMMap(streamIndex);
}
@@ -256,10 +282,22 @@ abstract class OboeAudioStream extends AudioStreamBase {
private native double getTimestampLatency(int streamIndex);
@Override
- public double getCpuLoad() {
+ public float getCpuLoad() {
return getCpuLoad(streamIndex);
}
- private native double getCpuLoad(int streamIndex);
+ private native float getCpuLoad(int streamIndex);
+
+ @Override
+ public float getAndResetMaxCpuLoad() {
+ return getAndResetMaxCpuLoad(streamIndex);
+ }
+ private native float getAndResetMaxCpuLoad(int streamIndex);
+
+ @Override
+ public int getAndResetCpuMask() {
+ return getAndResetCpuMask(streamIndex);
+ }
+ private native int getAndResetCpuMask(int streamIndex);
@Override
public String getCallbackTimeStr() {
@@ -268,7 +306,7 @@ abstract class OboeAudioStream extends AudioStreamBase {
public native String getCallbackTimeString();
@Override
- public native void setWorkload(double workload);
+ public native void setWorkload(int workload);
@Override
public int getState() {
@@ -278,6 +316,8 @@ abstract class OboeAudioStream extends AudioStreamBase {
public static native void setCallbackReturnStop(boolean b);
+ public static native void setHangTimeMillis(int hangTimeMillis);
+
public static native void setUseCallback(boolean checked);
public static native void setCallbackSize(int callbackSize);
diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/RoundTripLatencyActivity.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/RoundTripLatencyActivity.java
index 69207688..8f81f558 100644
--- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/RoundTripLatencyActivity.java
+++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/RoundTripLatencyActivity.java
@@ -21,12 +21,15 @@ import static com.mobileer.oboetester.IntentBasedTestSupport.configureStreamsFro
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
-import android.support.annotation.NonNull;
+import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
+import androidx.annotation.NonNull;
+import java.io.File;
import java.io.IOException;
+import java.util.Locale;
/**
* Activity to measure latency on a full duplex stream.
@@ -132,18 +135,18 @@ public class RoundTripLatencyActivity extends AnalyzerActivity {
mTimestampLatencies.calculateMeanAbsoluteDeviation(timestampLatencyMean);
}
message = "average.latency.msec = "
- + String.format(LATENCY_FORMAT, meanLatency) + "\n"
+ + String.format(Locale.getDefault(), LATENCY_FORMAT, meanLatency) + "\n"
+ "mean.absolute.deviation = "
- + String.format(LATENCY_FORMAT, meanAbsoluteDeviation) + "\n"
+ + String.format(Locale.getDefault(), LATENCY_FORMAT, meanAbsoluteDeviation) + "\n"
+ "average.confidence = "
- + String.format(CONFIDENCE_FORMAT, mAverageConfidence) + "\n"
- + "min.latency.msec = " + String.format(LATENCY_FORMAT, mLatencies.getMin()) + "\n"
- + "max.latency.msec = " + String.format(LATENCY_FORMAT, mLatencies.getMax()) + "\n"
+ + String.format(Locale.getDefault(), CONFIDENCE_FORMAT, mAverageConfidence) + "\n"
+ + "min.latency.msec = " + String.format(Locale.getDefault(), LATENCY_FORMAT, mLatencies.getMin()) + "\n"
+ + "max.latency.msec = " + String.format(Locale.getDefault(), LATENCY_FORMAT, mLatencies.getMax()) + "\n"
+ "num.iterations = " + mLatencies.count() + "\n"
+ "timestamp.latency.msec = "
- + String.format(LATENCY_FORMAT, timestampLatencyMean) + "\n"
+ + String.format(Locale.getDefault(), LATENCY_FORMAT, timestampLatencyMean) + "\n"
+ "timestamp.latency.mad = "
- + String.format(LATENCY_FORMAT, timestampLatencyMAD) + "\n";
+ + String.format(Locale.getDefault(), LATENCY_FORMAT, timestampLatencyMAD) + "\n";
}
message += "num.failed = " + mBadCount + "\n";
message += "\n"; // mark end of average report
@@ -208,7 +211,10 @@ public class RoundTripLatencyActivity extends AnalyzerActivity {
} else {
message = getResultString();
}
- onAnalyzerDone();
+ File resultFile = onAnalyzerDone();
+ if (resultFile != null) {
+ message = "result.file = " + resultFile.getAbsolutePath() + "\n" + message;
+ }
} else {
message = getProgressText();
message += "please wait... " + counter + "\n";
@@ -248,21 +254,23 @@ public class RoundTripLatencyActivity extends AnalyzerActivity {
int progress = getAnalyzerProgress();
int state = getAnalyzerState();
int resetCount = getResetCount();
- String message = String.format("progress = %d\nstate = %d\n#resets = %d\n",
+ String message = String.format(Locale.getDefault(), "progress = %d\nstate = %d\n#resets = %d\n",
progress, state, resetCount);
message += mAverageLatencyTestRunner.getLastReport();
return message;
}
- private void onAnalyzerDone() {
+ private File onAnalyzerDone() {
+ File resultFile = null;
if (mTestRunningByIntent) {
String report = getCommonTestReport();
report += getResultString();
- maybeWriteTestResult(report);
+ resultFile = maybeWriteTestResult(report);
}
mTestRunningByIntent = false;
mHasRecording = true;
stopAudioTest();
+ return resultFile;
}
@NonNull
@@ -272,8 +280,8 @@ public class RoundTripLatencyActivity extends AnalyzerActivity {
double confidence = getMeasuredConfidence();
String message = "";
- message += String.format("confidence = " + CONFIDENCE_FORMAT + "\n", confidence);
- message += String.format("result.text = %s\n", resultCodeToString(result));
+ message += String.format(Locale.getDefault(), "confidence = " + CONFIDENCE_FORMAT + "\n", confidence);
+ message += String.format(Locale.getDefault(), "result.text = %s\n", resultCodeToString(result));
// Only report valid latencies.
if (result == 0) {
@@ -282,26 +290,26 @@ public class RoundTripLatencyActivity extends AnalyzerActivity {
int bufferSize = mAudioOutTester.getCurrentAudioStream().getBufferSizeInFrames();
int latencyEmptyFrames = latencyFrames - bufferSize;
double latencyEmptyMillis = latencyEmptyFrames * 1000.0 / getSampleRate();
- 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);
+ message += String.format(Locale.getDefault(), "latency.msec = " + LATENCY_FORMAT + "\n", latencyMillis);
+ message += String.format(Locale.getDefault(), "latency.frames = %d\n", latencyFrames);
+ message += String.format(Locale.getDefault(), "latency.empty.msec = " + LATENCY_FORMAT + "\n", latencyEmptyMillis);
+ message += String.format(Locale.getDefault(), "latency.empty.frames = %d\n", latencyEmptyFrames);
}
- message += String.format("rms.signal = %7.5f\n", getSignalRMS());
- message += String.format("rms.noise = %7.5f\n", getBackgroundRMS());
- message += String.format("correlation = " + CONFIDENCE_FORMAT + "\n",
+ message += String.format(Locale.getDefault(), "rms.signal = %7.5f\n", getSignalRMS());
+ message += String.format(Locale.getDefault(), "rms.noise = %7.5f\n", getBackgroundRMS());
+ message += String.format(Locale.getDefault(), "correlation = " + CONFIDENCE_FORMAT + "\n",
getMeasuredCorrelation());
double timestampLatency = getTimestampLatencyMillis();
- message += String.format("timestamp.latency.msec = " + LATENCY_FORMAT + "\n",
+ message += String.format(Locale.getDefault(), "timestamp.latency.msec = " + LATENCY_FORMAT + "\n",
timestampLatency);
if (mTimestampLatencyStats.count() > 0) {
- message += String.format("timestamp.latency.mad = " + LATENCY_FORMAT + "\n",
+ message += String.format(Locale.getDefault(), "timestamp.latency.mad = " + LATENCY_FORMAT + "\n",
mTimestampLatencyStats.calculateMeanAbsoluteDeviation(timestampLatency));
}
message += "timestamp.latency.count = " + mTimestampLatencyStats.count() + "\n";
- message += String.format("reset.count = %d\n", resetCount);
- message += String.format("result = %d\n", result);
+ message += String.format(Locale.getDefault(), "reset.count = %d\n", resetCount);
+ message += String.format(Locale.getDefault(), "result = %d\n", result);
return message;
}
@@ -348,6 +356,9 @@ public class RoundTripLatencyActivity extends AnalyzerActivity {
hideSettingsViews();
mBufferSizeView.setFaderNormalizedProgress(0.0); // for lowest latency
+
+ mCommunicationDeviceView = (CommunicationDeviceView) findViewById(R.id.comm_device_view);
+
}
@Override
diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/StreamConfiguration.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/StreamConfiguration.java
index ea9a9e8c..5c1f32b7 100644
--- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/StreamConfiguration.java
+++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/StreamConfiguration.java
@@ -21,6 +21,7 @@ import android.content.res.Resources;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
+import java.util.Locale;
/**
* Container for the properties of a Stream.
@@ -44,6 +45,7 @@ public class StreamConfiguration {
public static final int AUDIO_FORMAT_PCM_FLOAT = 2; // must match AAUDIO
public static final int AUDIO_FORMAT_PCM_24 = 3; // must match AAUDIO
public static final int AUDIO_FORMAT_PCM_32 = 4; // must match AAUDIO
+ public static final int AUDIO_FORMAT_IEC61937 = 5; // must match AAUDIO
public static final int DIRECTION_OUTPUT = 0; // must match AAUDIO
public static final int DIRECTION_INPUT = 1; // must match AAUDIO
@@ -72,7 +74,25 @@ public class StreamConfiguration {
public static final int INPUT_PRESET_UNPROCESSED = 9; // must match Oboe
public static final int INPUT_PRESET_VOICE_PERFORMANCE = 10; // must match Oboe
+ public static final int ERROR_BASE = -900; // must match Oboe
public static final int ERROR_DISCONNECTED = -899; // must match Oboe
+ public static final int ERROR_ILLEGAL_ARGUMENT = -898; // must match Oboe
+ public static final int ERROR_INTERNAL = -896; // must match Oboe
+ public static final int ERROR_INVALID_STATE = -895; // must match Oboe
+ public static final int ERROR_INVALID_HANDLE = -892; // must match Oboe
+ public static final int ERROR_UNIMPLEMENTED = -890; // must match Oboe
+ public static final int ERROR_UNAVAILABLE = -889; // must match Oboe
+ public static final int ERROR_NO_FREE_HANDLES = -888; // must match Oboe
+ public static final int ERROR_NO_MEMORY = -887; // must match Oboe
+ public static final int ERROR_NULL = -886; // must match Oboe
+ public static final int ERROR_TIMEOUT = -885; // must match Oboe
+ public static final int ERROR_WOULD_BLOCK = -884; // must match Oboe
+ public static final int ERROR_INVALID_FORMAT = -883; // must match Oboe
+ public static final int ERROR_OUT_OF_RANGE = -882; // must match Oboe
+ public static final int ERROR_NO_SERVICE = -881; // must match Oboe
+ public static final int ERROR_INVALID_RATE = -880; // must match Oboe
+ public static final int ERROR_CLOSED = -869; // must match Oboe
+ public static final int ERROR_OK = 0; // must match Oboe
public static final int USAGE_MEDIA = 1;
public static final int USAGE_VOICE_COMMUNICATION = 2;
@@ -277,6 +297,9 @@ public class StreamConfiguration {
private int mFramesPerBurst;
private boolean mMMap;
private int mChannelMask;
+ private int mHardwareChannelCount;
+ private int mHardwareSampleRate;
+ private int mHardwareFormat;
public StreamConfiguration() {
reset();
@@ -327,6 +350,9 @@ public class StreamConfiguration {
mChannelConversionAllowed = false;
mRateConversionQuality = RATE_CONVERSION_QUALITY_NONE;
mMMap = NativeEngine.isMMapSupported();
+ mHardwareChannelCount = UNSPECIFIED;
+ mHardwareSampleRate = UNSPECIFIED;
+ mHardwareFormat = UNSPECIFIED;
}
public int getFramesPerBurst() {
@@ -486,6 +512,8 @@ public class StreamConfiguration {
return "I32";
case AUDIO_FORMAT_PCM_FLOAT:
return "Float";
+ case AUDIO_FORMAT_IEC61937:
+ return "IEC61937";
default:
return "Invalid";
}
@@ -563,6 +591,25 @@ public class StreamConfiguration {
}
}
+ static String convertRateConversionQualityToText(int quality) {
+ switch(quality) {
+ case RATE_CONVERSION_QUALITY_NONE:
+ return "None";
+ case RATE_CONVERSION_QUALITY_FASTEST:
+ return "Fastest";
+ case RATE_CONVERSION_QUALITY_LOW:
+ return "Low";
+ case RATE_CONVERSION_QUALITY_MEDIUM:
+ return "Medium";
+ case RATE_CONVERSION_QUALITY_HIGH:
+ return "High";
+ case RATE_CONVERSION_QUALITY_BEST:
+ return "Best";
+ default:
+ return "?=" + quality;
+ }
+ }
+
public static int convertTextToChannelMask(String text) {
return mChannelMaskStringToIntegerMap.get(text);
}
@@ -571,26 +618,30 @@ public class StreamConfiguration {
public String dump() {
String prefix = (getDirection() == DIRECTION_INPUT) ? "in" : "out";
StringBuffer message = new StringBuffer();
- message.append(String.format("%s.channels = %d\n", prefix, mChannelCount));
- message.append(String.format("%s.perf = %s\n", prefix,
- convertPerformanceModeToText(mPerformanceMode).toLowerCase()));
+ message.append(String.format(Locale.getDefault(), "%s.channels = %d\n", prefix, mChannelCount));
+ message.append(String.format(Locale.getDefault(), "%s.perf = %s\n", prefix,
+ convertPerformanceModeToText(mPerformanceMode).toLowerCase(Locale.getDefault())));
if (getDirection() == DIRECTION_INPUT) {
- message.append(String.format("%s.preset = %s\n", prefix,
- convertInputPresetToText(mInputPreset).toLowerCase()));
+ message.append(String.format(Locale.getDefault(), "%s.preset = %s\n", prefix,
+ convertInputPresetToText(mInputPreset).toLowerCase(Locale.getDefault())));
} else {
- message.append(String.format("%s.preset = %s\n", prefix,
- convertUsageToText(mUsage).toLowerCase()));
- message.append(String.format("%s.contentType = %s\n", prefix,
- convertContentTypeToText(mContentType).toLowerCase()));
+ message.append(String.format(Locale.getDefault(), "%s.usage = %s\n", prefix,
+ convertUsageToText(mUsage).toLowerCase(Locale.getDefault())));
+ message.append(String.format(Locale.getDefault(), "%s.contentType = %s\n", prefix,
+ convertContentTypeToText(mContentType).toLowerCase(Locale.getDefault())));
}
- message.append(String.format("%s.sharing = %s\n", prefix,
- convertSharingModeToText(mSharingMode).toLowerCase()));
- message.append(String.format("%s.api = %s\n", prefix,
- convertNativeApiToText(getNativeApi()).toLowerCase()));
- message.append(String.format("%s.rate = %d\n", prefix, mSampleRate));
- message.append(String.format("%s.device = %d\n", prefix, mDeviceId));
- message.append(String.format("%s.mmap = %s\n", prefix, isMMap() ? "yes" : "no"));
- message.append(String.format("%s.rate.conversion.quality = %d\n", prefix, mRateConversionQuality));
+ message.append(String.format(Locale.getDefault(), "%s.sharing = %s\n", prefix,
+ convertSharingModeToText(mSharingMode).toLowerCase(Locale.getDefault())));
+ message.append(String.format(Locale.getDefault(), "%s.api = %s\n", prefix,
+ convertNativeApiToText(getNativeApi()).toLowerCase(Locale.getDefault())));
+ message.append(String.format(Locale.getDefault(), "%s.rate = %d\n", prefix, mSampleRate));
+ message.append(String.format(Locale.getDefault(), "%s.device = %d\n", prefix, mDeviceId));
+ message.append(String.format(Locale.getDefault(), "%s.mmap = %s\n", prefix, isMMap() ? "yes" : "no"));
+ message.append(String.format(Locale.getDefault(), "%s.rate.conversion.quality = %d\n", prefix, mRateConversionQuality));
+ message.append(String.format(Locale.getDefault(), "%s.hardware.channels = %d\n", prefix, mHardwareChannelCount));
+ message.append(String.format(Locale.getDefault(), "%s.hardware.sampleRate = %d\n", prefix, mHardwareSampleRate));
+ message.append(String.format(Locale.getDefault(), "%s.hardware.format = %s\n", prefix,
+ convertFormatToText(mHardwareFormat).toLowerCase(Locale.getDefault())));
return message.toString();
}
@@ -622,7 +673,7 @@ public class StreamConfiguration {
}
private static boolean matchInputPreset(String text, int preset) {
- return convertInputPresetToText(preset).toLowerCase().equals(text);
+ return convertInputPresetToText(preset).toLowerCase(Locale.getDefault()).equals(text);
}
/**
@@ -631,7 +682,7 @@ public class StreamConfiguration {
* @return inputPreset, eg. INPUT_PRESET_CAMCORDER
*/
public static int convertTextToInputPreset(String text) {
- text = text.toLowerCase();
+ text = text.toLowerCase(Locale.getDefault());
if (matchInputPreset(text, INPUT_PRESET_GENERIC)) {
return INPUT_PRESET_GENERIC;
} else if (matchInputPreset(text, INPUT_PRESET_CAMCORDER)) {
@@ -726,4 +777,72 @@ public class StreamConfiguration {
return mChannelMaskStrings;
}
+ public int getHardwareChannelCount() {
+ return mHardwareChannelCount;
+ }
+
+ public void setHardwareChannelCount(int hardwareChannelCount) {
+ this.mHardwareChannelCount = hardwareChannelCount;
+ }
+
+ public int getHardwareSampleRate() {
+ return mHardwareSampleRate;
+ }
+
+ public void setHardwareSampleRate(int hardwareSampleRate) {
+ this.mHardwareSampleRate = hardwareSampleRate;
+ }
+
+ public int getHardwareFormat() {
+ return mHardwareFormat;
+ }
+
+ public void setHardwareFormat(int hardwareFormat) {
+ this.mHardwareFormat = hardwareFormat;
+ }
+
+ static String convertErrorToText(int error) {
+ switch (error) {
+ case ERROR_BASE:
+ return "ErrorBase";
+ case ERROR_DISCONNECTED:
+ return "ErrorDisconnected";
+ case ERROR_ILLEGAL_ARGUMENT:
+ return "ErrorIllegalArgument";
+ case ERROR_INTERNAL:
+ return "ErrorInternal";
+ case ERROR_INVALID_STATE:
+ return "ErrorInvalidState";
+ case ERROR_INVALID_HANDLE:
+ return "ErrorInvalidHandle";
+ case ERROR_UNIMPLEMENTED:
+ return "ErrorUnimplemented";
+ case ERROR_UNAVAILABLE:
+ return "ErrorUnavailable";
+ case ERROR_NO_FREE_HANDLES:
+ return "ErrorNoFreeHandles";
+ case ERROR_NO_MEMORY:
+ return "ErrorNoMemory";
+ case ERROR_NULL:
+ return "ErrorNull";
+ case ERROR_TIMEOUT:
+ return "ErrorTimeout";
+ case ERROR_WOULD_BLOCK:
+ return "ErrorWouldBlock";
+ case ERROR_INVALID_FORMAT:
+ return "ErrorInvalidFormat";
+ case ERROR_OUT_OF_RANGE:
+ return "ErrorOutOfRange";
+ case ERROR_NO_SERVICE:
+ return "ErrorNoService";
+ case ERROR_INVALID_RATE:
+ return "ErrorInvalidRate";
+ case ERROR_CLOSED:
+ return "ErrorClosed";
+ case ERROR_OK:
+ return "ErrorOk";
+ default:
+ return "?=" + error;
+ }
+ }
}
diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/StreamConfigurationView.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/StreamConfigurationView.java
index eeb962f0..418d08e5 100644
--- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/StreamConfigurationView.java
+++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/StreamConfigurationView.java
@@ -39,6 +39,8 @@ import android.util.Log;
import com.mobileer.audio_device.AudioDeviceListEntry;
import com.mobileer.audio_device.AudioDeviceSpinner;
+import java.util.Locale;
+
/**
* View for Editing a requested StreamConfiguration
* and displaying the actual StreamConfiguration.
@@ -63,7 +65,8 @@ public class StreamConfigurationView extends LinearLayout {
private Spinner mChannelMaskSpinner;
private TextView mActualChannelMaskView;
private TextView mActualFormatView;
-
+ private Spinner mCapacitySpinner;
+ private TextView mActualCapacityView;
private TableRow mInputPresetTableRow;
private Spinner mInputPresetSpinner;
private TextView mActualInputPresetView;
@@ -288,6 +291,10 @@ public class StreamConfigurationView extends LinearLayout {
});
mActualFormatView = (TextView) findViewById(R.id.actualAudioFormat);
mFormatSpinner = (Spinner) findViewById(R.id.spinnerFormat);
+
+ mActualCapacityView = (TextView) findViewById(R.id.actualCapacity);
+ mCapacitySpinner = (Spinner) findViewById(R.id.spinnerCapacity);
+
mRateConversionQualitySpinner = (Spinner) findViewById(R.id.spinnerSRCQuality);
mActualPerformanceView = (TextView) findViewById(R.id.actualPerformanceMode);
@@ -388,13 +395,17 @@ public class StreamConfigurationView extends LinearLayout {
int channelMask = StreamConfiguration.convertTextToChannelMask(text);
config.setChannelMask(channelMask);
config.setChannelCount(0);
- Log.d(TAG, String.format("Set channel mask as %s(%#x)", text, channelMask));
+ Log.d(TAG, String.format(Locale.getDefault(), "Set channel mask as %s(%#x)", text, channelMask));
} else {
config.setChannelCount(mChannelCountSpinner.getSelectedItemPosition());
config.setChannelMask(StreamConfiguration.UNSPECIFIED);
Log.d(TAG, "Set channel count as " + mChannelCountSpinner.getSelectedItemPosition());
}
+ text = mCapacitySpinner.getSelectedItem().toString();
+ int bufferCapacity = Integer.parseInt(text);
+ config.setBufferCapacityInFrames(bufferCapacity);
+
config.setMMap(mRequestedMMapView.isChecked());
config.setChannelConversionAllowed(mChannelConversionBox.isChecked());
config.setFormatConversionAllowed(mFormatConversionBox.isChecked());
@@ -418,6 +429,7 @@ public class StreamConfigurationView extends LinearLayout {
mFormatConversionBox.setEnabled(enabled);
mChannelCountSpinner.setEnabled(enabled);
mChannelMaskSpinner.setEnabled(enabled);
+ mCapacitySpinner.setEnabled(enabled);
mInputPresetSpinner.setEnabled(enabled);
mUsageSpinner.setEnabled(enabled);
mContentTypeSpinner.setEnabled(enabled);
@@ -465,14 +477,27 @@ public class StreamConfigurationView extends LinearLayout {
mActualSessionIdView.setText("S#: " + actualConfiguration.getSessionId());
value = actualConfiguration.getChannelMask();
mActualChannelMaskView.setText(StreamConfiguration.convertChannelMaskToText(value));
+ mActualCapacityView.setText(actualConfiguration.getBufferCapacityInFrames() + "");
boolean isMMap = actualConfiguration.isMMap();
- mStreamInfoView.setText("burst = " + actualConfiguration.getFramesPerBurst()
- + ", capacity = " + actualConfiguration.getBufferCapacityInFrames()
- + ", devID = " + actualConfiguration.getDeviceId()
- + ", " + (actualConfiguration.isMMap() ? "MMAP" : "Legacy")
- + (isMMap ? ", " + StreamConfiguration.convertSharingModeToText(sharingMode) : "")
- );
+
+ String msg = "";
+ msg += "burst = " + actualConfiguration.getFramesPerBurst();
+ msg += ", devID = " + actualConfiguration.getDeviceId();
+ msg += ", " + (actualConfiguration.isMMap() ? "MMAP" : "Legacy");
+ msg += (isMMap ? ", " + StreamConfiguration.convertSharingModeToText(sharingMode) : "");
+
+ int hardwareChannelCount = actualConfiguration.getHardwareChannelCount();
+ int hardwareSampleRate = actualConfiguration.getHardwareSampleRate();
+ int hardwareFormat = actualConfiguration.getHardwareFormat();
+ msg += "\nHW: #ch=" + (hardwareChannelCount ==
+ StreamConfiguration.UNSPECIFIED ? "?" : hardwareChannelCount);
+ msg += ", SR=" + (hardwareSampleRate ==
+ StreamConfiguration.UNSPECIFIED ? "?" : hardwareSampleRate);
+ msg += ", format=" + (hardwareFormat == StreamConfiguration.UNSPECIFIED ?
+ "?" : StreamConfiguration.convertFormatToText(hardwareFormat));
+
+ mStreamInfoView.setText(msg);
mHideableView.requestLayout();
}
@@ -531,7 +556,7 @@ public class StreamConfigurationView extends LinearLayout {
if (mAcousticEchoCanceler != null) {
mAcousticEchoCanceler.setEnabled(mAcousticEchoCancelerCheckBox.isChecked());
} else {
- Log.e(TAG, String.format("Could not create AcousticEchoCanceler"));
+ Log.e(TAG, String.format(Locale.getDefault(), "Could not create AcousticEchoCanceler"));
}
}
// If AGC is not available, the checkbox will be disabled in initializeViews().
@@ -540,7 +565,7 @@ public class StreamConfigurationView extends LinearLayout {
if (mAutomaticGainControl != null) {
mAutomaticGainControl.setEnabled(mAutomaticGainControlCheckBox.isChecked());
} else {
- Log.e(TAG, String.format("Could not create AutomaticGainControl"));
+ Log.e(TAG, String.format(Locale.getDefault(), "Could not create AutomaticGainControl"));
}
}
}
diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TapToToneActivity.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TapToToneActivity.java
index 2da6bf5f..ad2c3c97 100644
--- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TapToToneActivity.java
+++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TapToToneActivity.java
@@ -18,6 +18,8 @@ package com.mobileer.oboetester;
import android.Manifest;
import android.content.pm.PackageManager;
+import android.media.AudioDeviceInfo;
+import android.media.AudioManager;
import android.media.midi.MidiDevice;
import android.media.midi.MidiDeviceInfo;
import android.media.midi.MidiInputPort;
@@ -30,9 +32,13 @@ import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
+import android.view.WindowManager;
+import android.widget.AdapterView;
import android.widget.Button;
import android.widget.Toast;
+import com.mobileer.audio_device.AudioDeviceListEntry;
+import com.mobileer.audio_device.AudioDeviceSpinner;
import com.mobileer.miditools.MidiOutputPortConnectionSelector;
import com.mobileer.miditools.MidiPortConnector;
import com.mobileer.miditools.MidiTools;
@@ -59,6 +65,8 @@ public class TapToToneActivity extends TestOutputActivityBase {
private MidiOutputPortConnectionSelector mPortSelector;
private final MyNoteListener mTestListener = new MyNoteListener();
+ private AudioDeviceSpinner mInputDeviceSpinner;
+
@Override
protected void inflateActivity() {
setContentView(R.layout.activity_tap_to_tone);
@@ -103,11 +111,16 @@ public class TapToToneActivity extends TestOutputActivityBase {
return true;
});
+ mCommunicationDeviceView = (CommunicationDeviceView) findViewById(R.id.comm_device_view);
+
mStartButton = (Button) findViewById(R.id.button_start);
mStopButton = (Button) findViewById(R.id.button_stop);
updateButtons(false);
updateEnabledWidgets();
+
+ mInputDeviceSpinner = (AudioDeviceSpinner) findViewById(R.id.input_devices_spinner);
+ mInputDeviceSpinner.setDirectionType(AudioManager.GET_DEVICES_INPUTS);
}
private void updateButtons(boolean running) {
@@ -252,28 +265,6 @@ public class TapToToneActivity extends TestOutputActivityBase {
}
}
- @Override
- public boolean onCreateOptionsMenu(Menu menu) {
- // Inflate the menu; this adds items to the action bar if it is present.
- getMenuInflater().inflate(R.menu.menu_main, menu);
- return true;
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- // Handle action bar item clicks here. The action bar will
- // automatically handle clicks on the Home/Up button, so long
- // as you specify a parent activity in AndroidManifest.xml.
- int id = item.getItemId();
-
- //noinspection SimplifiableIfStatement
- if (id == R.id.action_settings) {
- return true;
- }
-
- return super.onOptionsItemSelected(item);
- }
-
public void startTest(View view) {
try {
openAudio();
@@ -284,8 +275,7 @@ public class TapToToneActivity extends TestOutputActivityBase {
}
try {
super.startAudio();
- mTapToToneTester.resetLatency();
- mTapToToneTester.start();
+ startTapToToneTester();
updateButtons(true);
} catch (IOException e) {
e.printStackTrace();
@@ -295,9 +285,25 @@ public class TapToToneActivity extends TestOutputActivityBase {
}
public void stopTest(View view) {
- mTapToToneTester.stop();
+ stopTapToToneTester();
stopAudio();
closeAudio();
updateButtons(false);
}
+
+ private void startTapToToneTester() throws IOException {
+ AudioDeviceInfo deviceInfo =
+ ((AudioDeviceListEntry) mInputDeviceSpinner.getSelectedItem()).getDeviceInfo();
+ mTapToToneTester.setInputDevice(deviceInfo);
+ mInputDeviceSpinner.setEnabled(false);
+ mTapToToneTester.resetLatency();
+ mTapToToneTester.start();
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ }
+
+ private void stopTapToToneTester() {
+ getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ mInputDeviceSpinner.setEnabled(true);
+ mTapToToneTester.stop();
+ }
}
diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TapToToneTester.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TapToToneTester.java
index bd931534..ce92a25a 100644
--- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TapToToneTester.java
+++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TapToToneTester.java
@@ -1,9 +1,11 @@
package com.mobileer.oboetester;
import android.app.Activity;
+import android.media.AudioDeviceInfo;
import android.widget.TextView;
import java.io.IOException;
+import java.util.Locale;
/**
* Measure tap-to-tone latency by and update the waveform display.
@@ -176,7 +178,7 @@ public class TapToToneTester {
mLatencyMax = latencyMillis;
}
- text = String.format("tap-to-tone latency = %3d msec\n", latencyMillis);
+ text = String.format(Locale.getDefault(), "tap-to-tone latency = %3d msec\n", latencyMillis);
}
mWaveformView.setSampleData(result.filtered);
}
@@ -185,7 +187,7 @@ public class TapToToneTester {
int averageLatencySamples = mLatencySumSamples / mMeasurementCount;
int averageLatencyMillis = 1000 * averageLatencySamples / result.frameRate;
final String plural = (mMeasurementCount == 1) ? "test" : "tests";
- text = text + String.format("min = %3d, avg = %3d, max = %3d, %d %s",
+ text = text + String.format(Locale.getDefault(), "min = %3d, avg = %3d, max = %3d, %d %s",
mLatencyMin, averageLatencyMillis, mLatencyMax, mMeasurementCount, plural);
}
final String postText = text;
@@ -198,4 +200,8 @@ public class TapToToneTester {
mArmed = true;
}
+
+ void setInputDevice(AudioDeviceInfo deviceInfo) {
+ mRecorder.setInputDevice(deviceInfo);
+ }
}
diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestAudioActivity.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestAudioActivity.java
index f61873fa..9043f28d 100644
--- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestAudioActivity.java
+++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestAudioActivity.java
@@ -16,34 +16,32 @@
package com.mobileer.oboetester;
-import android.Manifest;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
+import android.media.AudioAttributes;
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
-import android.support.annotation.NonNull;
-import android.support.v4.app.ActivityCompat;
-import android.support.v4.content.ContextCompat;
import android.util.Log;
import android.view.View;
import android.view.WindowManager;
+import android.widget.AdapterView;
import android.widget.Button;
import android.widget.CheckBox;
+import android.widget.Spinner;
import android.widget.Toast;
+import androidx.annotation.NonNull;
import java.io.File;
-import java.io.FileOutputStream;
import java.io.IOException;
-import java.io.OutputStreamWriter;
-import java.io.Writer;
import java.util.ArrayList;
+import java.util.Locale;
/**
* Base class for other Activities.
@@ -57,9 +55,11 @@ abstract class TestAudioActivity extends Activity {
public static final int AUDIO_STATE_OPEN = 0;
public static final int AUDIO_STATE_STARTED = 1;
public static final int AUDIO_STATE_PAUSED = 2;
- public static final int AUDIO_STATE_STOPPED = 3;
- public static final int AUDIO_STATE_CLOSING = 4;
- public static final int AUDIO_STATE_CLOSED = 5;
+ public static final int AUDIO_STATE_FLUSHED = 3;
+ public static final int AUDIO_STATE_STOPPED = 4;
+ public static final int AUDIO_STATE_RELEASED = 5;
+ public static final int AUDIO_STATE_CLOSING = 6;
+ public static final int AUDIO_STATE_CLOSED = 7;
public static final int COLOR_ACTIVE = 0xFFD0D0A0;
public static final int COLOR_IDLE = 0xFFD0D0D0;
@@ -75,8 +75,7 @@ abstract class TestAudioActivity extends Activity {
public static final int ACTIVITY_GLITCHES = 6;
public static final int ACTIVITY_TEST_DISCONNECT = 7;
public static final int ACTIVITY_DATA_PATHS = 8;
-
- private static final int MY_PERMISSIONS_REQUEST_EXTERNAL_STORAGE = 1001;
+ public static final int ACTIVITY_DYNAMIC_WORKLOAD = 9;
private int mAudioState = AUDIO_STATE_CLOSED;
@@ -84,10 +83,16 @@ abstract class TestAudioActivity extends Activity {
private Button mOpenButton;
private Button mStartButton;
private Button mPauseButton;
+ private Button mFlushButton;
private Button mStopButton;
+ private Button mReleaseButton;
private Button mCloseButton;
private MyStreamSniffer mStreamSniffer;
private CheckBox mCallbackReturnStopBox;
+ private Spinner mHangTimeSpinner;
+
+ // Only set in some activities
+ protected CommunicationDeviceView mCommunicationDeviceView;
private int mSampleRate;
private int mSingleTestIndex = -1;
private static boolean mBackgroundEnabled;
@@ -96,6 +101,7 @@ abstract class TestAudioActivity extends Activity {
protected boolean mTestRunningByIntent;
protected String mResultFileName;
private String mTestResults;
+ private ExternalFileWriter mExternalFileWriter = new ExternalFileWriter(this);
public String getTestName() {
return "TestAudio";
@@ -225,6 +231,10 @@ abstract class TestAudioActivity extends Activity {
super.onStart();
resetConfiguration();
setActivityType(getActivityType());
+ // TODO Use LifeCycleObserver instead of this.
+ if (mCommunicationDeviceView != null) {
+ mCommunicationDeviceView.onStart();
+ }
}
protected void resetConfiguration() {
@@ -289,6 +299,9 @@ abstract class TestAudioActivity extends Activity {
Log.i(TAG, "onStop() called so stop the test =========================");
onStopTest();
}
+ if (mCommunicationDeviceView != null) {
+ mCommunicationDeviceView.onStop();
+ }
super.onStop();
}
@@ -307,7 +320,9 @@ abstract class TestAudioActivity extends Activity {
mOpenButton.setBackgroundColor(mAudioState == AUDIO_STATE_OPEN ? COLOR_ACTIVE : COLOR_IDLE);
mStartButton.setBackgroundColor(mAudioState == AUDIO_STATE_STARTED ? COLOR_ACTIVE : COLOR_IDLE);
mPauseButton.setBackgroundColor(mAudioState == AUDIO_STATE_PAUSED ? COLOR_ACTIVE : COLOR_IDLE);
+ mFlushButton.setBackgroundColor(mAudioState == AUDIO_STATE_FLUSHED ? COLOR_ACTIVE : COLOR_IDLE);
mStopButton.setBackgroundColor(mAudioState == AUDIO_STATE_STOPPED ? COLOR_ACTIVE : COLOR_IDLE);
+ mReleaseButton.setBackgroundColor(mAudioState == AUDIO_STATE_RELEASED ? COLOR_ACTIVE : COLOR_IDLE);
mCloseButton.setBackgroundColor(mAudioState == AUDIO_STATE_CLOSED ? COLOR_ACTIVE : COLOR_IDLE);
}
setConfigViewsEnabled(mAudioState == AUDIO_STATE_CLOSED);
@@ -368,7 +383,6 @@ abstract class TestAudioActivity extends Activity {
if (streamContext.configurationView != null) {
streamContext.configurationView.setOutput(false);
}
- streamContext.tester = AudioInputTester.getInstance();
mStreamContexts.add(streamContext);
return streamContext;
}
@@ -407,7 +421,9 @@ abstract class TestAudioActivity extends Activity {
if (mOpenButton != null) {
mStartButton = (Button) findViewById(R.id.button_start);
mPauseButton = (Button) findViewById(R.id.button_pause);
+ mFlushButton = (Button) findViewById(R.id.button_flush);
mStopButton = (Button) findViewById(R.id.button_stop);
+ mReleaseButton = (Button) findViewById(R.id.button_release);
mCloseButton = (Button) findViewById(R.id.button_close);
}
mStreamContexts = new ArrayList<StreamContext>();
@@ -423,6 +439,24 @@ abstract class TestAudioActivity extends Activity {
}
OboeAudioStream.setCallbackReturnStop(false);
+ mHangTimeSpinner = (Spinner) findViewById(R.id.spinner_hang_time);
+ if (mHangTimeSpinner != null) {
+ mHangTimeSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
+ public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+ String hangTimeText = (String) mHangTimeSpinner.getAdapter().getItem(position);
+ int hangTimeMillis = Integer.parseInt(hangTimeText);
+ Log.d(TAG, "Hang Time = " + hangTimeMillis + " msec");
+
+ OboeAudioStream.setHangTimeMillis(hangTimeMillis);
+ }
+
+ public void onNothingSelected(AdapterView<?> parent) {
+ OboeAudioStream.setHangTimeMillis(0);
+ }
+ });
+ }
+ OboeAudioStream.setHangTimeMillis(0);
+
mStreamSniffer = new MyStreamSniffer();
}
@@ -438,8 +472,8 @@ abstract class TestAudioActivity extends Activity {
}
protected void showErrorToast(String message) {
+ Log.e(TAG, "showErrorToast(\"" + message + "\")");
String text = "Error: " + message;
- Log.e(TAG, text);
showToast(text);
}
@@ -454,20 +488,39 @@ abstract class TestAudioActivity extends Activity {
});
}
+ private void onStartAllContexts() {
+ for (StreamContext streamContext : mStreamContexts) {
+ streamContext.tester.getCurrentAudioStream().onStart();
+ }
+ }
+ private void onStopAllContexts() {
+ for (StreamContext streamContext : mStreamContexts) {
+ streamContext.tester.getCurrentAudioStream().onStop();
+ }
+ }
+
public void openAudio(View view) {
try {
openAudio();
} catch (Exception e) {
- showErrorToast(e.getMessage());
+ showErrorToast("openAudio() caught " + e.getMessage());
+ }
+ }
+
+ void clearHangTime() {
+ OboeAudioStream.setHangTimeMillis(0);
+ if (mHangTimeSpinner != null) {
+ mHangTimeSpinner.setSelection(0);
}
}
public void startAudio(View view) {
Log.i(TAG, "startAudio() called =======================================");
+ clearHangTime(); // start running
try {
startAudio();
} catch (Exception e) {
- showErrorToast(e.getMessage());
+ showErrorToast("startAudio() caught " + e.getMessage());
}
keepScreenOn(true);
}
@@ -490,10 +543,18 @@ abstract class TestAudioActivity extends Activity {
keepScreenOn(false);
}
+ public void flushAudio(View view) {
+ flushAudio();
+ }
+
public void closeAudio(View view) {
closeAudio();
}
+ public void releaseAudio(View view) {
+ releaseAudio();
+ }
+
public int getSampleRate() {
return mSampleRate;
}
@@ -507,18 +568,25 @@ abstract class TestAudioActivity extends Activity {
applyConfigurationViewsToModels();
}
- int sampleRate = 0;
+ int sampleRate = 0; // Use the OUTPUT sample rate for INPUT
// Open output streams then open input streams.
// This is so that the capacity of input stream can be expanded to
// match the burst size of the output for full duplex.
for (StreamContext streamContext : mStreamContexts) {
- if (!streamContext.isInput()) {
+ if (!streamContext.isInput()) { // OUTPUT?
openStreamContext(streamContext);
int streamSampleRate = streamContext.tester.actualConfiguration.getSampleRate();
if (sampleRate == 0) {
sampleRate = streamSampleRate;
}
+
+ if (shouldSetStreamControlByAttributes()) {
+ // Associate volume keys with this output stream.
+ int actualUsage = streamContext.tester.actualConfiguration.getUsage();
+ int actualContentType = streamContext.tester.actualConfiguration.getContentType();
+ setStreamControlByAttributes(actualUsage, actualContentType);
+ }
}
}
for (StreamContext streamContext : mStreamContexts) {
@@ -530,9 +598,29 @@ abstract class TestAudioActivity extends Activity {
}
}
updateEnabledWidgets();
+ onStartAllContexts();
mStreamSniffer.startStreamSniffer();
}
+ protected boolean shouldSetStreamControlByAttributes() {
+ return true;
+ }
+
+ /**
+ * Associate the volume keys with the stream we are playing.
+ * @param usage usage for the stream
+ * @param contentType tupe of the stream
+ */
+ private void setStreamControlByAttributes(int usage, int contentType) {
+ AudioAttributes attributes = new AudioAttributes.Builder().setUsage(usage)
+ .setContentType(contentType).build();
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ int volumeControlStream = attributes.getVolumeControlStream();
+ Log.i(TAG, "setVolumeControlStream(" + volumeControlStream + ")");
+ setVolumeControlStream(volumeControlStream);
+ }
+ }
+
/**
* @param deviceId
* @return true if the device is TYPE_BLUETOOTH_SCO
@@ -564,7 +652,7 @@ abstract class TestAudioActivity extends Activity {
try {
streamContext.configurationView.setupEffects(sessionId);
} catch (Exception e) {
- showErrorToast(e.getMessage());
+ showErrorToast("openStreamContext() caught " + e.getMessage());
}
}
streamContext.configurationView.updateDisplay(streamContext.tester.actualConfiguration);
@@ -576,21 +664,28 @@ abstract class TestAudioActivity extends Activity {
private native int pauseNative();
+ private native int flushNative();
+
private native int stopNative();
+ private native int releaseNative();
+
protected native void setActivityType(int activityType);
private native int getFramesPerCallback();
+ public native void setUseAlternativeAdpf(boolean enabled);
+
private static native void setDefaultAudioValues(int audioManagerSampleRate, int audioManagerFramesPerBurst);
public void startAudio() throws IOException {
Log.i(TAG, "startAudio() called =========================");
int result = startNative();
- if (result < 0) {
- showErrorToast("Start failed with " + result);
- throw new IOException("startNative returned " + result);
+ if (result != 0) {
+ showErrorToast("Start failed with " + result + ", " + StreamConfiguration.convertErrorToText(result));
+ throw new IOException("startNative returned " + result + ", " + StreamConfiguration.convertErrorToText(result));
} else {
+ onStartAllContexts();
for (StreamContext streamContext : mStreamContexts) {
StreamConfigurationView configView = streamContext.configurationView;
if (configView != null) {
@@ -603,26 +698,49 @@ abstract class TestAudioActivity extends Activity {
}
protected void toastPauseError(int result) {
- showErrorToast("Pause failed with " + result);
+ showErrorToast("Pause failed with " + result + ", " + StreamConfiguration.convertErrorToText(result));
}
public void pauseAudio() {
int result = pauseNative();
- if (result < 0) {
+ if (result != 0) {
toastPauseError(result);
} else {
mAudioState = AUDIO_STATE_PAUSED;
updateEnabledWidgets();
+ onStopAllContexts();
+ }
+ }
+
+ public void flushAudio() {
+ int result = flushNative();
+ if (result != 0) {
+ showErrorToast("Flush failed with " + result + ", " + StreamConfiguration.convertErrorToText(result));
+ } else {
+ mAudioState = AUDIO_STATE_FLUSHED;
+ updateEnabledWidgets();
}
}
public void stopAudio() {
int result = stopNative();
- if (result < 0) {
- showErrorToast("Stop failed with " + result);
+ if (result != 0) {
+ showErrorToast("Stop failed with " + result + ", " + StreamConfiguration.convertErrorToText(result));
} else {
mAudioState = AUDIO_STATE_STOPPED;
updateEnabledWidgets();
+ onStopAllContexts();
+ }
+ }
+
+ public void releaseAudio() {
+ int result = releaseNative();
+ if (result != 0) {
+ showErrorToast("Release failed with " + result + ", " + StreamConfiguration.convertErrorToText(result));
+ } else {
+ mAudioState = AUDIO_STATE_RELEASED;
+ updateEnabledWidgets();
+ onStopAllContexts();
}
}
@@ -684,24 +802,6 @@ abstract class TestAudioActivity extends Activity {
myAudioMgr.stopBluetoothSco();
}
- @Override
- public void onRequestPermissionsResult(int requestCode,
- String[] permissions,
- int[] grantResults) {
-
- if (MY_PERMISSIONS_REQUEST_EXTERNAL_STORAGE != requestCode) {
- super.onRequestPermissionsResult(requestCode, permissions, grantResults);
- return;
- }
- // If request is cancelled, the result arrays are empty.
- if (grantResults.length > 0
- && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
- writeTestResult(mTestResults);
- } else {
- showToast("Writing external storage needed for test results.");
- }
- }
-
@NonNull
protected String getCommonTestReport() {
StringBuffer report = new StringBuffer();
@@ -709,8 +809,8 @@ abstract class TestAudioActivity extends Activity {
report.append("build.fingerprint = " + Build.FINGERPRINT + "\n");
try {
PackageInfo pinfo = getPackageManager().getPackageInfo(getPackageName(), 0);
- report.append(String.format("test.version = %s\n", pinfo.versionName));
- report.append(String.format("test.version.code = %d\n", pinfo.versionCode));
+ report.append(String.format(Locale.getDefault(), "test.version = %s\n", pinfo.versionName));
+ report.append(String.format(Locale.getDefault(), "test.version.code = %d\n", pinfo.versionCode));
} catch (PackageManager.NameNotFoundException e) {
}
report.append("time.millis = " + System.currentTimeMillis() + "\n");
@@ -733,48 +833,17 @@ abstract class TestAudioActivity extends Activity {
return report.toString();
}
- void writeTestResultIfPermitted(String resultString) {
- // Here, thisActivity is the current activity
- if (ContextCompat.checkSelfPermission(this,
- Manifest.permission.WRITE_EXTERNAL_STORAGE)
- != PackageManager.PERMISSION_GRANTED) {
- mTestResults = resultString;
- ActivityCompat.requestPermissions(this,
- new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
- MY_PERMISSIONS_REQUEST_EXTERNAL_STORAGE);
- } else {
- // Permission has already been granted
- writeTestResult(resultString);
- }
- }
-
- void maybeWriteTestResult(String resultString) {
+ File maybeWriteTestResult(String resultString) {
+ File fileWritten = null;
if (mResultFileName != null) {
- writeTestResultIfPermitted(resultString);
- };
- }
-
- // Run this in a background thread.
- void writeTestResult(String resultString) {
- File resultFile = new File(mResultFileName);
- Writer writer = null;
- try {
- writer = new OutputStreamWriter(new FileOutputStream(resultFile));
- writer.write(resultString);
- } catch (
- IOException e) {
- e.printStackTrace();
- showErrorToast(" writing result file. " + e.getMessage());
- } finally {
- if (writer != null) {
- try {
- writer.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
+ try {
+ fileWritten = mExternalFileWriter.writeStringToExternalFile(resultString, mResultFileName);
+ } catch (IOException e) {
+ e.printStackTrace();
+ showErrorToast(" writing result file. " + e.getMessage());
}
+ mResultFileName = null;
}
-
- mResultFileName = null;
+ return fileWritten;
}
}
diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestColdStartLatencyActivity.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestColdStartLatencyActivity.java
new file mode 100644
index 00000000..b88b611c
--- /dev/null
+++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestColdStartLatencyActivity.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.mobileer.oboetester;
+
+import static com.mobileer.oboetester.TestAudioActivity.TAG;
+
+import android.app.Activity;
+import android.content.Context;
+import android.media.AudioManager;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.RadioButton;
+import android.widget.Spinner;
+import android.widget.TextView;
+
+import java.util.Random;
+
+/**
+ * Test for getting the cold start latency
+ */
+public class TestColdStartLatencyActivity extends Activity {
+
+ private TextView mStatusView;
+ private MyStreamSniffer mStreamSniffer;
+ private AudioManager mAudioManager;
+ private RadioButton mOutputButton;
+ private RadioButton mInputButton;
+ private CheckBox mLowLatencyCheckBox;
+ private CheckBox mMmapCheckBox;
+ private CheckBox mExclusiveCheckBox;
+ private Spinner mStartStabilizeDelaySpinner;
+ private Spinner mCloseOpenDelaySpinner;
+ private Spinner mOpenStartDelaySpinner;
+ private Button mStartButton;
+ private Button mStopButton;
+
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_cold_start_latency);
+ mStatusView = (TextView) findViewById(R.id.text_status);
+ mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
+
+ mStartButton = (Button) findViewById(R.id.button_start_test);
+ mStopButton = (Button) findViewById(R.id.button_stop_test);
+ mOutputButton = (RadioButton) findViewById(R.id.direction_output);
+ mInputButton = (RadioButton) findViewById(R.id.direction_input);
+ mMmapCheckBox = (CheckBox) findViewById(R.id.checkbox_mmap);
+ mExclusiveCheckBox = (CheckBox) findViewById(R.id.checkbox_exclusive);
+ mLowLatencyCheckBox = (CheckBox) findViewById(R.id.checkbox_low_latency);
+ mStartStabilizeDelaySpinner = (Spinner) findViewById(R.id.spinner_start_stabilize_time);
+ mStartStabilizeDelaySpinner.setSelection(7); // Set to 1000 ms by default
+ mCloseOpenDelaySpinner = (Spinner) findViewById(R.id.spinner_close_open_time);
+ mOpenStartDelaySpinner = (Spinner) findViewById(R.id.spinner_open_start_time);
+
+ setButtonsEnabled(false);
+ }
+
+ public void onStartColdStartLatencyTest(View view) {
+ keepScreenOn(true);
+ stopSniffer();
+ mStreamSniffer = new MyStreamSniffer();
+ mStreamSniffer.start();
+ setButtonsEnabled(true);
+ }
+
+ public void onStopColdStartLatencyTest(View view) {
+ keepScreenOn(false);
+ stopSniffer();
+ setButtonsEnabled(false);
+ }
+
+ private void setButtonsEnabled(boolean running) {
+ mStartButton.setEnabled(!running);
+ mStopButton.setEnabled(running);
+ mOutputButton.setEnabled(!running);
+ mInputButton.setEnabled(!running);
+ mLowLatencyCheckBox.setEnabled(!running);
+ mMmapCheckBox.setEnabled(!running);
+ mExclusiveCheckBox.setEnabled(!running);
+ mStartStabilizeDelaySpinner.setEnabled(!running);
+ mCloseOpenDelaySpinner.setEnabled(!running);
+ mOpenStartDelaySpinner.setEnabled(!running);
+ }
+
+ protected class MyStreamSniffer extends Thread {
+ boolean enabled = true;
+ StringBuffer statusBuffer = new StringBuffer();
+ int loopCount;
+
+ @Override
+ public void run() {
+ boolean useInput = mInputButton.isChecked();
+ boolean useLowLatency = mLowLatencyCheckBox.isChecked();
+ boolean useMmap = mMmapCheckBox.isChecked();
+ boolean useExclusive = mExclusiveCheckBox.isChecked();
+ Log.d(TAG,(useInput ? "IN" : "OUT")
+ + ", " + (useLowLatency ? "LOW_LATENCY" : "NOT LOW_LATENCY")
+ + ", " + (useMmap ? "MMAP" : "NOT MMAP")
+ + ", " + (useExclusive ? "EXCLUSIVE" : "SHARED"));
+ String closeSleepTimeText =
+ (String) mCloseOpenDelaySpinner.getAdapter().getItem(
+ mCloseOpenDelaySpinner.getSelectedItemPosition());
+ int closedSleepTimeMillis = Integer.parseInt(closeSleepTimeText);
+ Log.d(TAG, "Sleep before open time = " + closedSleepTimeMillis + " msec");
+ String openSleepTimeText = (String) mOpenStartDelaySpinner.getAdapter().getItem(
+ mOpenStartDelaySpinner.getSelectedItemPosition());
+ int openSleepTimeMillis = Integer.parseInt(openSleepTimeText);
+ Log.d(TAG, "Sleep after open Time = " + openSleepTimeMillis + " msec");
+ String startStabilizeTimeText = (String) mStartStabilizeDelaySpinner.getAdapter().getItem(
+ mStartStabilizeDelaySpinner.getSelectedItemPosition());
+ int startSleepTimeMillis = Integer.parseInt(startStabilizeTimeText);
+ Log.d(TAG, "Sleep after start Time = " + startSleepTimeMillis + " msec");
+ while (enabled) {
+ loopCount++;
+ try {
+ sleep(closedSleepTimeMillis);
+ openStream(useInput, useLowLatency, useMmap, useExclusive);
+ log("-------#" + loopCount + " Device Id: " + getAudioDeviceId());
+ log("open() Latency: " + getOpenTimeMicros() / 1000 + " msec");
+ sleep(openSleepTimeMillis);
+ startStream();
+ log("requestStart() Latency: " + getStartTimeMicros() / 1000 + " msec");
+ sleep(startSleepTimeMillis);
+ log("Cold Start Latency: " + getColdStartTimeMicros() / 1000 + " msec");
+ closeStream();
+ } catch (InterruptedException e) {
+ enabled = false;
+ } finally {
+ closeStream();
+ }
+ }
+ }
+
+ // Log to screen and logcat.
+ private void log(String text) {
+ statusBuffer.append(text + "\n");
+ showStatus(statusBuffer.toString());
+ }
+
+ // Stop the test thread.
+ void finish() {
+ enabled = false;
+ interrupt();
+ try {
+ join(2000);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ protected void showStatus(final String message) {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mStatusView.setText(message);
+ }
+ });
+ }
+
+ private native int openStream(boolean useInput, boolean useLowLatency, boolean useMmap,
+ boolean useExclusive);
+ private native int startStream();
+ private native int closeStream();
+ private native int getOpenTimeMicros();
+ private native int getStartTimeMicros();
+ private native int getColdStartTimeMicros();
+ private native int getAudioDeviceId();
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ stopSniffer();
+ }
+
+ private void stopSniffer() {
+ if (mStreamSniffer != null) {
+ mStreamSniffer.finish();
+ mStreamSniffer = null;
+ }
+ }
+
+ protected void keepScreenOn(boolean on) {
+ if (on) {
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ } else {
+ getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ }
+ }
+}
diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestDataPathsActivity.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestDataPathsActivity.java
index b0aea932..74879f14 100644
--- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestDataPathsActivity.java
+++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestDataPathsActivity.java
@@ -17,49 +17,74 @@
package com.mobileer.oboetester;
import static com.mobileer.oboetester.IntentBasedTestSupport.configureStreamsFromBundle;
+import static com.mobileer.oboetester.StreamConfiguration.convertChannelMaskToText;
-import android.app.Activity;
-import android.content.Context;
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
+import android.os.Build;
import android.os.Bundle;
-import android.os.Handler;
-import android.os.Looper;
-import android.support.annotation.NonNull;
import android.util.Log;
import android.widget.CheckBox;
+import android.widget.RadioButton;
+import android.widget.RadioGroup;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
import com.mobileer.audio_device.AudioDeviceInfoConverter;
import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.Locale;
/**
- * 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.
+ * Play a recognizable tone on each channel of an output device
+ * and listen for the result through an input.
+ * Test various channels, InputPresets, ChannelMasks and SampleRates.
+ *
+ * Select device types based on priority of attached peripherals.
+ * Print devices types being tested.
*
* 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.
+ * signal is or whether it is corrupted. A very noisy room may have
+ * lots of energy at the target frequency but the phase will be random.
*
- * This test requires a quiet room but no other hardware.
+ * This test requires a quiet room if you are testing speaker/mic pairs.
+ * It can also be used to test using analog loopback adapters
+ * or USB devices configured in loopback mode.
*/
public class TestDataPathsActivity extends BaseAutoGlitchActivity {
public static final String KEY_USE_INPUT_PRESETS = "use_input_presets";
public static final boolean VALUE_DEFAULT_USE_INPUT_PRESETS = true;
- public static final String KEY_USE_INPUT_DEVICES = "use_input_devices";
- public static final boolean VALUE_DEFAULT_USE_INPUT_DEVICES = true;
+ public static final String KEY_USE_ALL_SAMPLE_RATES = "use_all_sample_rates";
+ public static final boolean VALUE_DEFAULT_USE_ALL_SAMPLE_RATES = false;
+
+ public static final String KEY_SINGLE_TEST_INDEX = "single_test_index";
+ public static final int VALUE_DEFAULT_SINGLE_TEST_INDEX = -1;
+ public static final String KEY_USE_ALL_CHANNEL_COUNTS = "use_all_channel_counts";
+ public static final boolean VALUE_DEFAULT_USE_ALL_CHANNEL_COUNTS = true;
+ public static final String KEY_USE_INPUT_CHANNEL_MASKS = "use_input_channel_masks";
+ public static final boolean VALUE_DEFAULT_USE_INPUT_CHANNEL_MASKS = false;
+ public static final String KEY_OUTPUT_CHANNEL_MASKS_LEVEL = "output_channel_masks_level";
+
+ // The following KEYs are for old deprecated commands.
+ public static final String KEY_USE_ALL_OUTPUT_CHANNEL_MASKS = "use_all_output_channel_masks";
+ public static final boolean VALUE_DEFAULT_USE_ALL_OUTPUT_CHANNEL_MASKS = false;
+
+ // How many tests should be run in a specific category, eg. channel masks?
+ private static final int COVERAGE_LEVEL_NONE = 0;
+ private static final int COVERAGE_LEVEL_SOME = 1;
+ private static final int COVERAGE_LEVEL_ALL = 2;
+ public static final String KEY_USE_INPUT_DEVICES = "use_input_devices";
+ public static final boolean VALUE_DEFAULT_USE_INPUT_DEVICES = false;
public static final String KEY_USE_OUTPUT_DEVICES = "use_output_devices";
public static final boolean VALUE_DEFAULT_USE_OUTPUT_DEVICES = true;
- public static final String KEY_SINGLE_TEST_INDEX = "single_test_index";
- public static final int VALUE_DEFAULT_SINGLE_TEST_INDEX = -1;
public static final int DURATION_SECONDS = 3;
private final static double MIN_REQUIRED_MAGNITUDE = 0.001;
@@ -70,6 +95,42 @@ public class TestDataPathsActivity extends BaseAutoGlitchActivity {
private final static double MAX_ALLOWED_JITTER = 2.0 * PHASE_PER_BIN;
private final static String MAGNITUDE_FORMAT = "%7.5f";
+ // These define the values returned by the Java API deviceInfo.getChannelMasks().
+ public static final int JAVA_CHANNEL_IN_LEFT = 1 << 2; // AudioFormat.CHANNEL_IN_LEFT
+ public static final int JAVA_CHANNEL_IN_RIGHT = 1 << 3; // AudioFormat.CHANNEL_IN_RIGHT
+ public static final int JAVA_CHANNEL_IN_FRONT = 1 << 4; // AudioFormat.CHANNEL_IN_FRONT
+ public static final int JAVA_CHANNEL_IN_BACK = 1 << 5; // AudioFormat.CHANNEL_IN_BACK
+
+ // These do not have corresponding Java definitions.
+ // They match definitions in system/media/audio/include/system/audio-hal-enums.h
+ public static final int JAVA_CHANNEL_IN_BACK_LEFT = 1 << 16;
+ public static final int JAVA_CHANNEL_IN_BACK_RIGHT = 1 << 17;
+ public static final int JAVA_CHANNEL_IN_CENTER = 1 << 18;
+ public static final int JAVA_CHANNEL_IN_LOW_FREQUENCY = 1 << 20;
+ public static final int JAVA_CHANNEL_IN_TOP_LEFT = 1 << 21;
+ public static final int JAVA_CHANNEL_IN_TOP_RIGHT = 1 << 22;
+
+ public static final int JAVA_CHANNEL_IN_MONO = JAVA_CHANNEL_IN_FRONT;
+ public static final int JAVA_CHANNEL_IN_STEREO = JAVA_CHANNEL_IN_LEFT | JAVA_CHANNEL_IN_RIGHT;
+ public static final int JAVA_CHANNEL_IN_FRONT_BACK = JAVA_CHANNEL_IN_FRONT | JAVA_CHANNEL_IN_BACK;
+ public static final int JAVA_CHANNEL_IN_2POINT0POINT2 = JAVA_CHANNEL_IN_LEFT |
+ JAVA_CHANNEL_IN_RIGHT |
+ JAVA_CHANNEL_IN_TOP_LEFT |
+ JAVA_CHANNEL_IN_TOP_RIGHT;
+ public static final int JAVA_CHANNEL_IN_2POINT1POINT2 =
+ JAVA_CHANNEL_IN_2POINT0POINT2 | JAVA_CHANNEL_IN_LOW_FREQUENCY;
+ public static final int JAVA_CHANNEL_IN_3POINT0POINT2 =
+ JAVA_CHANNEL_IN_2POINT0POINT2 | JAVA_CHANNEL_IN_CENTER;
+ public static final int JAVA_CHANNEL_IN_3POINT1POINT2 =
+ JAVA_CHANNEL_IN_3POINT0POINT2 | JAVA_CHANNEL_IN_LOW_FREQUENCY;
+ public static final int JAVA_CHANNEL_IN_5POINT1 = JAVA_CHANNEL_IN_LEFT |
+ JAVA_CHANNEL_IN_CENTER |
+ JAVA_CHANNEL_IN_RIGHT |
+ JAVA_CHANNEL_IN_BACK_LEFT |
+ JAVA_CHANNEL_IN_BACK_RIGHT |
+ JAVA_CHANNEL_IN_LOW_FREQUENCY;
+ public static final int JAVA_CHANNEL_UNDEFINED = -1;
+
final int TYPE_BUILTIN_SPEAKER_SAFE = 0x18; // API 30
private double mMagnitude;
@@ -79,22 +140,73 @@ public class TestDataPathsActivity extends BaseAutoGlitchActivity {
private double mPhaseErrorSum;
private double mPhaseErrorCount;
- AudioManager mAudioManager;
private CheckBox mCheckBoxInputPresets;
- private CheckBox mCheckBoxInputDevices;
- private CheckBox mCheckBoxOutputDevices;
+ private CheckBox mCheckBoxAllChannels;
+ private CheckBox mCheckBoxInputChannelMasks;
+ private RadioGroup mRadioGroupOutputChannelMasks;
+ private RadioButton mRadioOutputChannelMasksNone;
+ private RadioButton mRadioOutputChannelMasksSome;
+ private RadioButton mRadioOutputChannelMasksAll;
+ private CheckBox mCheckBoxAllSampleRates;
+ private TextView mInstructionsTextView;
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.
- StreamConfiguration.INPUT_PRESET_VOICE_COMMUNICATION,
StreamConfiguration.INPUT_PRESET_UNPROCESSED,
+ // Do not use INPUT_PRESET_VOICE_COMMUNICATION because AEC kills the signal.
+ StreamConfiguration.INPUT_PRESET_VOICE_RECOGNITION,
StreamConfiguration.INPUT_PRESET_VOICE_PERFORMANCE,
};
+ private static final int[] SHORT_OUTPUT_CHANNEL_MASKS = {
+ StreamConfiguration.CHANNEL_MONO,
+ StreamConfiguration.CHANNEL_STEREO,
+ StreamConfiguration.CHANNEL_2POINT1, // Smallest mask with more than two channels.
+ StreamConfiguration.CHANNEL_5POINT1, // This mask is very common.
+ StreamConfiguration.CHANNEL_7POINT1POINT4, // More than 8 channels might break.
+ };
+
+ private static final int[] ALL_OUTPUT_CHANNEL_MASKS = {
+ StreamConfiguration.CHANNEL_MONO,
+ StreamConfiguration.CHANNEL_STEREO,
+ StreamConfiguration.CHANNEL_2POINT1,
+ StreamConfiguration.CHANNEL_TRI,
+ StreamConfiguration.CHANNEL_TRI_BACK,
+ StreamConfiguration.CHANNEL_3POINT1,
+ StreamConfiguration.CHANNEL_2POINT0POINT2,
+ StreamConfiguration.CHANNEL_2POINT1POINT2,
+ StreamConfiguration.CHANNEL_3POINT0POINT2,
+ StreamConfiguration.CHANNEL_3POINT1POINT2,
+ StreamConfiguration.CHANNEL_QUAD,
+ StreamConfiguration.CHANNEL_QUAD_SIDE,
+ StreamConfiguration.CHANNEL_SURROUND,
+ StreamConfiguration.CHANNEL_PENTA,
+ StreamConfiguration.CHANNEL_5POINT1,
+ StreamConfiguration.CHANNEL_5POINT1_SIDE,
+ StreamConfiguration.CHANNEL_6POINT1,
+ StreamConfiguration.CHANNEL_7POINT1,
+ StreamConfiguration.CHANNEL_5POINT1POINT2,
+ StreamConfiguration.CHANNEL_5POINT1POINT4,
+ StreamConfiguration.CHANNEL_7POINT1POINT2,
+ StreamConfiguration.CHANNEL_7POINT1POINT4,
+ };
+
+ private static final int[] SAMPLE_RATES = {
+ 8000,
+ 11025,
+ 12000,
+ 16000,
+ 22050,
+ 24000,
+ 32000,
+ 44100,
+ 48000,
+ 64000,
+ 88200,
+ 96000,
+ };
+
@NonNull
public static String comparePassedField(String prefix, Object failed, Object passed, String name) {
try {
@@ -109,16 +221,25 @@ public class TestDataPathsActivity extends BaseAutoGlitchActivity {
return "ERROR - cannot access " + name;
}
}
+ public static String comparePassedInputPreset(String prefix, TestResult failed, TestResult passed) {
+ int failedValue = failed.inputPreset;
+ int passedValue = passed.inputPreset;
+ return (failedValue == passedValue) ? ""
+ : (prefix + " inPreset: passed = "
+ + StreamConfiguration.convertInputPresetToText(passedValue)
+ + ", failed = "
+ + StreamConfiguration.convertInputPresetToText(failedValue)
+ + "\n");
+ }
public static double calculatePhaseError(double p1, double p2) {
- double diff = Math.abs(p1 - p2);
+ double diff = p1 - p2;
// Wrap around the circle.
- while (diff > (2 * Math.PI)) {
- diff -= (2 * Math.PI);
+ while (diff > Math.PI) {
+ diff -= 2 * Math.PI;
}
- // A phase error close to 2*PI is actually a small phase error.
- if (diff > Math.PI) {
- diff = (2 * Math.PI) - diff;
+ while (diff < -Math.PI) {
+ diff += 2 * Math.PI;
}
return diff;
}
@@ -126,10 +247,6 @@ public class TestDataPathsActivity extends BaseAutoGlitchActivity {
// 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;
@@ -141,30 +258,28 @@ public class TestDataPathsActivity extends BaseAutoGlitchActivity {
super.startSniffer();
}
- @Override
- public void run() {
+ private void gatherData() {
mMagnitude = getMagnitude();
mMaxMagnitude = getMaxMagnitude();
- Log.d(TAG, String.format("magnitude = %7.4f, maxMagnitude = %7.4f",
+ Log.d(TAG, String.format(Locale.getDefault(), "magnitude = %7.4f, maxMagnitude = %7.4f",
mMagnitude, mMaxMagnitude));
// Only look at the phase if we have a signal.
if (mMagnitude >= MIN_REQUIRED_MAGNITUDE) {
- double phase = getPhase();
+ double phase = getPhaseDataPaths();
// Wait for the analyzer to get a lock on the signal.
// Arbitrary number of phase measurements before we start measuring jitter.
final int kMinPhaseMeasurementsRequired = 4;
if (mPhaseCount >= kMinPhaseMeasurementsRequired) {
- double phaseError = calculatePhaseError(phase, mPhase);
- // low pass filter
+ double phaseError = Math.abs(calculatePhaseError(phase, mPhase));
+ // collect average error
mPhaseErrorSum += phaseError;
mPhaseErrorCount++;
- Log.d(TAG, String.format("phase = %7.4f, diff = %7.4f, jitter = %7.4f",
- phase, phaseError, getAveragePhaseError()));
+ Log.d(TAG, String.format(Locale.getDefault(), "phase = %7.4f, mPhase = %7.4f, phaseError = %7.4f, jitter = %7.4f",
+ phase, mPhase, phaseError, getAveragePhaseError()));
}
mPhase = phase;
mPhaseCount++;
}
- reschedule();
}
public String getCurrentStatusReport() {
@@ -179,7 +294,6 @@ public class TestDataPathsActivity extends BaseAutoGlitchActivity {
return message.toString();
}
- @Override
public String getShortReport() {
return "maxMag = " + getMagnitudeText(mMaxMagnitude)
+ ", jitter = " + getJitterText();
@@ -187,6 +301,7 @@ public class TestDataPathsActivity extends BaseAutoGlitchActivity {
@Override
public void updateStatusText() {
+ gatherData();
mLastGlitchReport = getCurrentStatusReport();
runOnUiThread(() -> {
setAnalyzerText(mLastGlitchReport);
@@ -194,18 +309,34 @@ public class TestDataPathsActivity extends BaseAutoGlitchActivity {
}
}
+ // Write to status and command view
+ private void setInstructionsText(final String text) {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mInstructionsTextView.setText(text);
+ }
+ });
+ }
+
private String getJitterText() {
return isPhaseJitterValid() ? getMagnitudeText(getAveragePhaseError()) : "?";
}
@Override
NativeSniffer createNativeSniffer() {
- return new TestDataPathsActivity.DataPathSniffer(this);
+ return new TestDataPathsActivity.DataPathSniffer();
+ }
+
+ @Override
+ public String getShortReport() {
+ return ((DataPathSniffer) mNativeSniffer).getShortReport();
}
native double getMagnitude();
native double getMaxMagnitude();
- native double getPhase();
+
+ native double getPhaseDataPaths();
@Override
protected void inflateActivity() {
@@ -215,10 +346,18 @@ public class TestDataPathsActivity extends BaseAutoGlitchActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
mCheckBoxInputPresets = (CheckBox)findViewById(R.id.checkbox_paths_input_presets);
- mCheckBoxInputDevices = (CheckBox)findViewById(R.id.checkbox_paths_input_devices);
- mCheckBoxOutputDevices = (CheckBox)findViewById(R.id.checkbox_paths_output_devices);
+ mCheckBoxAllChannels = (CheckBox)findViewById(R.id.checkbox_paths_all_channels);
+ mCheckBoxInputChannelMasks = (CheckBox)findViewById(R.id.checkbox_paths_in_channel_masks);
+ mCheckBoxAllSampleRates =
+ (CheckBox)findViewById(R.id.checkbox_paths_all_sample_rates);
+
+ mInstructionsTextView = (TextView) findViewById(R.id.text_instructions);
+
+ mRadioGroupOutputChannelMasks = (RadioGroup) findViewById(R.id.group_ch_mask_options);
+ mRadioOutputChannelMasksNone = (RadioButton) findViewById(R.id.radio_out_ch_masks_none);
+ mRadioOutputChannelMasksSome = (RadioButton) findViewById(R.id.radio_out_ch_masks_some);
+ mRadioOutputChannelMasksAll = (RadioButton) findViewById(R.id.radio_out_ch_masks_all);
}
@Override
@@ -232,7 +371,7 @@ public class TestDataPathsActivity extends BaseAutoGlitchActivity {
}
static String getMagnitudeText(double value) {
- return String.format(MAGNITUDE_FORMAT, value);
+ return String.format(Locale.getDefault(), MAGNITUDE_FORMAT, value);
}
protected String getConfigText(StreamConfiguration config) {
@@ -244,35 +383,27 @@ public class TestDataPathsActivity extends BaseAutoGlitchActivity {
}
@Override
- protected String shouldTestBeSkipped() {
- String why = "";
+ protected String whyShouldTestBeSkipped() {
+ String why = super.whyShouldTestBeSkipped();
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 data path we requested.
- if (actualInConfig.isMMap() != requestedInConfig.isMMap()) {
- log("Did not get requested MMap input stream");
- why += "mmap";
- }
- if (actualOutConfig.isMMap() != requestedOutConfig.isMMap()) {
- log("Did not get requested MMap output 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() + ")";
+ why += "inDev(" + requestedInConfig.getDeviceId()
+ + "!=" + actualInConfig.getDeviceId() + "),";
}
if (requestedOutConfig.getDeviceId() != 0
&& (requestedOutConfig.getDeviceId() != actualOutConfig.getDeviceId())) {
why += ", outDev(" + requestedOutConfig.getDeviceId()
- + "!=" + actualOutConfig.getDeviceId() + ")";
+ + "!=" + actualOutConfig.getDeviceId() + "),";
}
if ((requestedInConfig.getInputPreset() != actualInConfig.getInputPreset())) {
why += ", inPre(" + requestedInConfig.getInputPreset()
- + "!=" + actualInConfig.getInputPreset() + ")";
+ + "!=" + actualInConfig.getInputPreset() + "),";
}
return why;
}
@@ -288,10 +419,6 @@ public class TestDataPathsActivity extends BaseAutoGlitchActivity {
@Override
public String didTestFail() {
String why = "";
- StreamConfiguration requestedInConfig = mAudioInputTester.requestedConfiguration;
- StreamConfiguration requestedOutConfig = mAudioOutTester.requestedConfiguration;
- StreamConfiguration actualInConfig = mAudioInputTester.actualConfiguration;
- StreamConfiguration actualOutConfig = mAudioOutTester.actualConfiguration;
if (mMaxMagnitude <= MIN_REQUIRED_MAGNITUDE) {
why += ", mag";
}
@@ -322,280 +449,387 @@ public class TestDataPathsActivity extends BaseAutoGlitchActivity {
+ ", IN" + (actualInConfig.isMMap() ? "-M" : "-L")
+ " D=" + actualInConfig.getDeviceId()
+ ", ch=" + channelText(getInputChannel(), actualInConfig.getChannelCount())
+ + ", SR=" + actualInConfig.getSampleRate()
+ ", OUT" + (actualOutConfig.isMMap() ? "-M" : "-L")
+ " D=" + actualOutConfig.getDeviceId()
+ ", ch=" + channelText(getOutputChannel(), actualOutConfig.getChannelCount())
+ + ", SR=" + actualOutConfig.getSampleRate()
+ ", mag = " + getMagnitudeText(mMaxMagnitude);
}
- void setupDeviceCombo(int 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);
- }
-
- private TestResult testConfigurationsAddMagJitter() throws InterruptedException {
- TestResult testResult = testConfigurations();
+ @Override
+ protected TestResult testCurrentConfigurations() throws InterruptedException {
+ TestResult testResult = super.testCurrentConfigurations();
if (testResult != null) {
testResult.addComment("mag = " + TestDataPathsActivity.getMagnitudeText(mMagnitude)
+ ", jitter = " + getJitterText());
- }
- return testResult;
- }
-
- 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;
- TestResult testResult = testConfigurationsAddMagJitter();
- if (testResult != null) {
+ logOneLineSummary(testResult);
int result = testResult.result;
- String summary = getOneLineSummary()
- + ", inPre = "
- + StreamConfiguration.convertInputPresetToText(inputPreset)
- + "\n";
- appendSummary(summary);
if (result == TEST_RESULT_FAILED) {
- if (getMagnitude() < 0.000001) {
- testResult.addComment("The input is completely SILENT!");
- } else if (inputPreset == StreamConfiguration.INPUT_PRESET_VOICE_COMMUNICATION) {
- testResult.addComment("Maybe sine wave blocked by Echo Cancellation!");
+ int id = mAudioOutTester.actualConfiguration.getDeviceId();
+ int deviceType = getDeviceInfoById(id).getType();
+ int channelCount = mAudioOutTester.actualConfiguration.getChannelCount();
+ if (deviceType == AudioDeviceInfo.TYPE_BUILTIN_EARPIECE
+ && channelCount == 2
+ && getOutputChannel() == 1) {
+ testResult.addComment("Maybe EARPIECE does not mix stereo to mono!");
+ }
+ if (deviceType == TYPE_BUILTIN_SPEAKER_SAFE
+ && channelCount == 2
+ && getOutputChannel() == 0) {
+ testResult.addComment("Maybe SPEAKER_SAFE dropped channel zero!");
}
}
}
+ return testResult;
}
- 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 {
- setTestName("Test InPreset = " + StreamConfiguration.convertInputPresetToText(inputPreset));
- testPresetCombo(inputPreset, 1, 0, 1, 0);
+ private void logSection(String name) {
+ logBoth("\n#" + (getTestCount() + 1) + " ########### " + name + "\n");
}
private void testInputPresets() throws InterruptedException {
- logBoth("\nTest InputPreset -------");
-
+ logSection("InputPreset");
+ StreamConfiguration requestedInConfig = mAudioInputTester.requestedConfiguration;
+ int originalPreset = requestedInConfig.getInputPreset();
for (int inputPreset : INPUT_PRESETS) {
- testPresetCombo(inputPreset);
+ requestedInConfig.setInputPreset(inputPreset);
+ testPerformancePaths();
+ }
+ requestedInConfig.setInputPreset(originalPreset);
+ }
+
+ // The native out channel mask is its channel mask shifted right by 2 bits.
+ // See AudioFormat.convertChannelOutMaskToNativeMask()
+ int convertJavaOutChannelMaskToNativeChannelMask(int javaChannelMask) {
+ return javaChannelMask >> 2;
+ }
+
+ // The native channel mask in AAudio and Oboe is different than the Java IN channel mask.
+ // See AAudioConvert_aaudioToAndroidChannelLayoutMask()
+ int convertJavaInChannelMaskToNativeChannelMask(int javaChannelMask) {
+ switch (javaChannelMask) {
+ case JAVA_CHANNEL_IN_MONO:
+ return StreamConfiguration.CHANNEL_MONO;
+ case JAVA_CHANNEL_IN_STEREO:
+ return StreamConfiguration.CHANNEL_STEREO;
+ case JAVA_CHANNEL_IN_FRONT_BACK:
+ return StreamConfiguration.CHANNEL_FRONT_BACK;
+ case JAVA_CHANNEL_IN_2POINT0POINT2:
+ return StreamConfiguration.CHANNEL_2POINT0POINT2;
+ case JAVA_CHANNEL_IN_2POINT1POINT2:
+ return StreamConfiguration.CHANNEL_2POINT1POINT2;
+ case JAVA_CHANNEL_IN_3POINT0POINT2:
+ return StreamConfiguration.CHANNEL_3POINT0POINT2;
+ case JAVA_CHANNEL_IN_3POINT1POINT2:
+ return StreamConfiguration.CHANNEL_3POINT1POINT2;
+ case JAVA_CHANNEL_IN_5POINT1:
+ return StreamConfiguration.CHANNEL_5POINT1;
+ default:
+ log("Unimplemented java channel mask: " + javaChannelMask + "\n");
+ return JAVA_CHANNEL_UNDEFINED;
}
-// 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);
+ void logOneLineSummary(TestResult testResult) {
+ logOneLineSummary(testResult, "");
+ }
- mMagnitude = -1.0;
- TestResult testResult = testConfigurationsAddMagJitter();
- if (testResult != null) {
- appendSummary(getOneLineSummary() + "\n");
+ void logOneLineSummary(TestResult testResult, String extra) {
+ int result = testResult.result;
+ String oneLineSummary;
+ if (result == TEST_RESULT_SKIPPED) {
+ oneLineSummary = "#" + mAutomatedTestRunner.getTestCount() + extra + ", SKIP";
+ } else if (result == TEST_RESULT_FAILED) {
+ oneLineSummary = getOneLineSummary() + extra + ", FAIL";
+ } else {
+ oneLineSummary = getOneLineSummary() + extra;
}
+ appendSummary(oneLineSummary + "\n");
}
- void testInputDeviceCombo(int deviceId,
- int deviceType,
- int numInputChannels,
- int inputChannel) throws InterruptedException {
+ void logBoth(String text) {
+ log(text);
+ appendSummary(text + "\n");
+ }
- String typeString = AudioDeviceInfoConverter.typeToString(deviceType);
- setTestName("Test InDev: #" + deviceId + " " + typeString
- + "_" + inputChannel + "/" + numInputChannels);
- 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?!
- int deviceType = deviceInfo.getType();
- if (deviceType == AudioDeviceInfo.TYPE_BUILTIN_MIC) {
- int id = deviceInfo.getId();
- int[] channelCounts = deviceInfo.getChannelCounts();
- numTested++;
- // Always test mono and stereo.
- testInputDeviceCombo(id, deviceType, 1, 0);
- testInputDeviceCombo(id, deviceType, 2, 0);
- testInputDeviceCombo(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++) {
- testInputDeviceCombo(id, deviceType, numChannels, channel);
- }
- }
- }
- }
- } else {
- log("Device skipped for type.");
- }
+ void logFailed(String text) {
+ log(text);
+ logAnalysis(text + "\n");
+ }
+
+ private void testDeviceOutputInfo(AudioDeviceInfo outputDeviceInfo) throws InterruptedException {
+ AudioDeviceInfo inputDeviceInfo = findCompatibleInputDevice(outputDeviceInfo.getType());
+ showDeviceInfo(outputDeviceInfo, inputDeviceInfo);
+ if (inputDeviceInfo == null) {
+ return;
+ }
+
+ StreamConfiguration requestedInConfig = mAudioInputTester.requestedConfiguration;
+ StreamConfiguration requestedOutConfig = mAudioOutTester.requestedConfiguration;
+ requestedInConfig.reset();
+ requestedOutConfig.reset();
+ requestedInConfig.setDeviceId(inputDeviceInfo.getId());
+ requestedOutConfig.setDeviceId(outputDeviceInfo.getId());
+ resetChannelConfigurations(requestedInConfig, requestedOutConfig);
+
+ if (mCheckBoxAllChannels.isChecked()) {
+ runOnUiThread(() -> mCheckBoxAllChannels.setEnabled(false));
+ testOutputChannelCounts(inputDeviceInfo, outputDeviceInfo);
+ }
+
+ if (mCheckBoxInputPresets.isChecked()) {
+ runOnUiThread(() -> mCheckBoxInputPresets.setEnabled(false));
+ testInputPresets();
+ }
+
+ if (mCheckBoxAllSampleRates.isChecked()) {
+ logSection("Sample Rates");
+ runOnUiThread(() -> mCheckBoxAllSampleRates.setEnabled(false));
+ for (int sampleRate : SAMPLE_RATES) {
+ requestedInConfig.setSampleRate(sampleRate);
+ requestedOutConfig.setSampleRate(sampleRate);
+ testPerformancePaths();
+ }
+ }
+ requestedInConfig.setSampleRate(0);
+ requestedOutConfig.setSampleRate(0);
+
+ // Channel Masks added to AAudio API in S_V2
+ if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.S_V2
+ && isDeviceTypeMixedForLoopback(outputDeviceInfo.getType())) {
+
+ if (mCheckBoxInputChannelMasks.isChecked()) { // INPUT?
+ logSection("Input Channel Masks");
+ runOnUiThread(() -> mCheckBoxInputChannelMasks.setEnabled(false));
+
+ resetChannelConfigurations(requestedInConfig, requestedOutConfig);
+ requestedInConfig.setChannelCount(0);
+ // Test the reported channel masks.
+ int[] channelMasks = inputDeviceInfo.getChannelMasks();
+ if (channelMasks.length > 0) {
+ for (int channelMask : channelMasks) {
+ int nativeChannelMask =
+ convertJavaInChannelMaskToNativeChannelMask(channelMask);
+ if (nativeChannelMask == JAVA_CHANNEL_UNDEFINED) {
+ log("channelMask: " + channelMask + " not supported. Skipping.\n");
+ continue;
+ }
+ log("\n#### nativeChannelMask = "
+ + convertChannelMaskToText(nativeChannelMask) + "\n");
+ int channelCount = Integer.bitCount(nativeChannelMask);
+ requestedInConfig.setChannelMask(nativeChannelMask);
+ for (int channel = 0; channel < channelCount; channel++) {
+ setInputChannel(channel);
+ testPerformancePaths();
+ }
+ }
+ }
+ resetChannelConfigurations(requestedInConfig, requestedOutConfig);
+ }
+
+ runOnUiThread(() -> mRadioGroupOutputChannelMasks.setEnabled(false));
+ if (!mRadioOutputChannelMasksNone.isChecked()) { // OUTPUT?
+ logSection("Output Channel Masks");
+ requestedInConfig.setChannelCount(1);
+ for (int channelMask : mRadioOutputChannelMasksAll.isChecked() ?
+ ALL_OUTPUT_CHANNEL_MASKS : SHORT_OUTPUT_CHANNEL_MASKS) {
+ int channelCount = Integer.bitCount(channelMask);
+ requestedOutConfig.setChannelMask(channelMask);
+ for (int channel = 0; channel < channelCount; channel++) {
+ setOutputChannel(channel);
+ testPerformancePaths();
+ }
+ }
+ resetChannelConfigurations(requestedInConfig, requestedOutConfig);
+ }
+ }
+ }
+
+ private void resetChannelConfigurations(StreamConfiguration requestedInConfig, StreamConfiguration requestedOutConfig) {
+ requestedInConfig.setChannelMask(0);
+ requestedOutConfig.setChannelMask(0);
+ requestedInConfig.setChannelCount(1);
+ requestedOutConfig.setChannelCount(1);
+ setInputChannel(0);
+ setOutputChannel(0);
+ }
+
+ private void showDeviceInfo(AudioDeviceInfo outputDeviceInfo, AudioDeviceInfo inputDeviceInfo) {
+ String deviceText = "OUT: type = "
+ + AudioDeviceInfoConverter.typeToString(outputDeviceInfo.getType())
+ + ", #ch = " + findLargestChannelCount(outputDeviceInfo.getChannelCounts());
+
+ setInstructionsText(deviceText);
+
+ if (inputDeviceInfo == null) {
+ deviceText += "ERROR - cannot find compatible device type for input!";
+ } else {
+ deviceText = "IN: type = "
+ + AudioDeviceInfoConverter.typeToString(inputDeviceInfo.getType())
+ + ", #ch = " + findLargestChannelCount(inputDeviceInfo.getChannelCounts())
+ + "\n" + deviceText;
}
+ setInstructionsText(deviceText);
+ }
- if (numTested == 0) {
- log("NO INPUT DEVICE FOUND!\n");
+ public static int findLargestChannelCount(int[] arr) {
+ if (arr == null || arr.length == 0) {
+ return 2;
}
+ return findLargestInt(arr);
}
- 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);
+ public static int findLargestInt(int[] arr) {
+ if (arr == null || arr.length == 0) {
+ throw new IllegalArgumentException("Array cannot be empty");
+ }
+
+ int max = arr[0];
+ for (int i = 1; i < arr.length; i++) {
+ if (arr[i] > max) {
+ max = arr[i];
+ }
+ }
+ return max;
+ }
+ private void testOutputChannelCounts(AudioDeviceInfo inputDeviceInfo, AudioDeviceInfo outputDeviceInfo) throws InterruptedException {
+ logSection("Output Channel Counts");
+ ArrayList<Integer> channelCountsTested =new ArrayList<Integer>();
+ StreamConfiguration requestedInConfig = mAudioInputTester.requestedConfiguration;
StreamConfiguration requestedOutConfig = mAudioOutTester.requestedConfiguration;
- requestedOutConfig.setDeviceId(deviceId);
- requestedOutConfig.setMMap(mmapEnabled);
- mMagnitude = -1.0;
- TestResult testResult = testConfigurationsAddMagJitter();
- if (testResult != null) {
- int result = testResult.result;
- appendSummary(getOneLineSummary() + "\n");
- if (result == TEST_RESULT_FAILED) {
- if (deviceType == AudioDeviceInfo.TYPE_BUILTIN_EARPIECE
- && numOutputChannels == 2
- && outputChannel == 1) {
- testResult.addComment("Maybe EARPIECE does not mix stereo to mono!");
+ int[] outputChannelCounts = outputDeviceInfo.getChannelCounts();
+ if (isDeviceTypeMixedForLoopback(outputDeviceInfo.getType())) {
+ requestedInConfig.setChannelCount(1);
+ setInputChannel(0);
+ // test mono
+ requestedOutConfig.setChannelCount(1);
+ channelCountsTested.add(1);
+ setOutputChannel(0);
+ testPerformancePaths();
+ // test stereo
+ requestedOutConfig.setChannelCount(2);
+ channelCountsTested.add(2);
+ setOutputChannel(0);
+ testPerformancePaths();
+ setOutputChannel(1);
+ testPerformancePaths();
+ // Test channels for each channelCount above 2
+ for (int numChannels : outputChannelCounts) {
+ log("numChannels = " + numChannels);
+ if (numChannels > 4) {
+ log("numChannels forced to 4!");
}
- if (deviceType == TYPE_BUILTIN_SPEAKER_SAFE
- && numOutputChannels == 2
- && outputChannel == 0) {
- testResult.addComment("Maybe SPEAKER_SAFE dropped channel zero!");
+ if (!channelCountsTested.contains(numChannels)) {
+ requestedOutConfig.setChannelCount(numChannels);
+ channelCountsTested.add(numChannels);
+ for (int channel = 0; channel < numChannels; channel++) {
+ setOutputChannel(channel);
+ testPerformancePaths();
+ }
+ }
+ }
+ } else {
+ // test mono
+ testMatchingChannels(1);
+ channelCountsTested.add(1);
+ // Test two matching stereo channels.
+ testMatchingChannels(2);
+ channelCountsTested.add(2);
+ // Test matching channels for each channelCount above 2
+ for (int numChannels : outputChannelCounts) {
+ log("numChannels = " + numChannels);
+ if (numChannels > 4) {
+ log("numChannels forced to 4!");
+ numChannels = 4;
+ }
+ if (!channelCountsTested.contains(numChannels)) {
+ testMatchingChannels(numChannels);
+ channelCountsTested.add(numChannels);
}
}
}
+ // Restore defaults.
+ requestedInConfig.setChannelCount(1);
+ setInputChannel(0);
+ requestedOutConfig.setChannelCount(1);
+ setOutputChannel(0);
+ }
+
+ private void testMatchingChannels(int numChannels) throws InterruptedException {
+ mAudioInputTester.requestedConfiguration.setChannelCount(numChannels);
+ mAudioOutTester.requestedConfiguration.setChannelCount(numChannels);
+ for (int channel = 0; channel < numChannels; channel++) {
+ setInputChannel(channel);
+ setOutputChannel(channel);
+ testPerformancePaths();
+ }
}
- void testOutputDeviceCombo(int deviceId,
- int deviceType,
- int numOutputChannels,
- int outputChannel) throws InterruptedException {
- String typeString = AudioDeviceInfoConverter.typeToString(deviceType);
- setTestName("Test OutDev: #" + deviceId + " " + typeString
- + "_" + outputChannel + "/" + numOutputChannels);
+ private void testPerformancePaths() throws InterruptedException {
+ StreamConfiguration requestedInConfig = mAudioInputTester.requestedConfiguration;
+ StreamConfiguration requestedOutConfig = mAudioOutTester.requestedConfiguration;
+
+ requestedInConfig.setSharingMode(StreamConfiguration.SHARING_MODE_SHARED);
+ requestedOutConfig.setSharingMode(StreamConfiguration.SHARING_MODE_SHARED);
+
+ // Legacy NONE
+ requestedInConfig.setMMap(false);
+ requestedOutConfig.setMMap(false);
+ requestedInConfig.setPerformanceMode(StreamConfiguration.PERFORMANCE_MODE_NONE);
+ requestedOutConfig.setPerformanceMode(StreamConfiguration.PERFORMANCE_MODE_NONE);
+ testCurrentConfigurations();
+
+ // Legacy LOW_LATENCY
+ requestedInConfig.setPerformanceMode(StreamConfiguration.PERFORMANCE_MODE_LOW_LATENCY);
+ requestedOutConfig.setPerformanceMode(StreamConfiguration.PERFORMANCE_MODE_LOW_LATENCY);
+ testCurrentConfigurations();
+
+ // MMAP LowLatency
if (NativeEngine.isMMapSupported()) {
- testOutputDeviceCombo(deviceId, deviceType, numOutputChannels, outputChannel, true);
+ requestedInConfig.setMMap(true);
+ requestedOutConfig.setMMap(true);
+ testCurrentConfigurations();
}
- testOutputDeviceCombo(deviceId, deviceType, numOutputChannels, outputChannel, false);
- }
-
- void logBoth(String text) {
- log(text);
- appendSummary(text + "\n");
- }
+ requestedInConfig.setMMap(false);
+ requestedOutConfig.setMMap(false);
- void logFailed(String text) {
- log(text);
- logAnalysis(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.");
- }
+ private void testOutputDeviceTypes() throws InterruptedException {
+ // Determine which output device type to test based on priorities.
+ AudioDeviceInfo info = getDeviceInfoByType(AudioDeviceInfo.TYPE_USB_DEVICE,
+ AudioManager.GET_DEVICES_OUTPUTS);
+ if (info != null) {
+ testDeviceOutputInfo(info);
+ return;
+ }
+ info = getDeviceInfoByType(AudioDeviceInfo.TYPE_USB_HEADSET,
+ AudioManager.GET_DEVICES_OUTPUTS);
+ if (info != null) {
+ testDeviceOutputInfo(info);
+ return;
}
- if (numTested == 0) {
- log("NO OUTPUT DEVICE FOUND!\n");
+ info = getDeviceInfoByType(AudioDeviceInfo.TYPE_WIRED_HEADSET,
+ AudioManager.GET_DEVICES_OUTPUTS);
+ if (info != null) {
+ testDeviceOutputInfo(info);
+ return;
+ }
+ // Test both SPEAKER and SPEAKER_SAFE
+ info = getDeviceInfoByType(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER,
+ AudioManager.GET_DEVICES_OUTPUTS);
+ if (info != null) {
+ testDeviceOutputInfo(info);
+ // Continue on to SPEAKER_SAFE
+ }
+ info = getDeviceInfoByType(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER_SAFE,
+ AudioManager.GET_DEVICES_OUTPUTS);
+ if (info != null) {
+ testDeviceOutputInfo(info);
}
}
@@ -610,31 +844,25 @@ public class TestDataPathsActivity extends BaseAutoGlitchActivity {
mTestResults.clear();
mDurationSeconds = DURATION_SECONDS;
- if (mCheckBoxInputPresets.isChecked()) {
- runOnUiThread(() -> mCheckBoxInputPresets.setEnabled(false));
- testInputPresets();
- }
- if (mCheckBoxInputDevices.isChecked()) {
- runOnUiThread(() -> mCheckBoxInputDevices.setEnabled(false));
- testInputDevices();
- }
- if (mCheckBoxOutputDevices.isChecked()) {
- runOnUiThread(() -> mCheckBoxOutputDevices.setEnabled(false));
- testOutputDevices();
- }
+ runOnUiThread(() -> keepScreenOn(true));
- analyzeTestResults();
+ testOutputDeviceTypes();
+
+ compareFailedTestsWithNearestPassingTest();
} catch (InterruptedException e) {
- analyzeTestResults();
+ compareFailedTestsWithNearestPassingTest();
} catch (Exception e) {
log(e.getMessage());
showErrorToast(e.getMessage());
} finally {
runOnUiThread(() -> {
mCheckBoxInputPresets.setEnabled(true);
- mCheckBoxInputDevices.setEnabled(true);
- mCheckBoxOutputDevices.setEnabled(true);
+ mCheckBoxAllChannels.setEnabled(true);
+ mCheckBoxInputChannelMasks.setEnabled(true);
+ mRadioGroupOutputChannelMasks.setEnabled(true);
+ mCheckBoxAllSampleRates.setEnabled(true);
+ keepScreenOn(false);
});
}
}
@@ -645,22 +873,59 @@ public class TestDataPathsActivity extends BaseAutoGlitchActivity {
StreamConfiguration requestedOutConfig = mAudioOutTester.requestedConfiguration;
configureStreamsFromBundle(mBundleFromIntent, requestedInConfig, requestedOutConfig);
- boolean shouldUseInputPresets = mBundleFromIntent.getBoolean(KEY_USE_INPUT_PRESETS,
+ // These are the current supported options.
+ final boolean shouldUseInputPresets = mBundleFromIntent.getBoolean(KEY_USE_INPUT_PRESETS,
VALUE_DEFAULT_USE_INPUT_PRESETS);
- boolean shouldUseInputDevices = mBundleFromIntent.getBoolean(KEY_USE_INPUT_DEVICES,
- VALUE_DEFAULT_USE_INPUT_DEVICES);
- boolean shouldUseOutputDevices = mBundleFromIntent.getBoolean(KEY_USE_OUTPUT_DEVICES,
- VALUE_DEFAULT_USE_OUTPUT_DEVICES);
- int singleTestIndex = mBundleFromIntent.getInt(KEY_SINGLE_TEST_INDEX,
+ final boolean shouldUseAllSampleRates =
+ mBundleFromIntent.getBoolean(KEY_USE_ALL_SAMPLE_RATES,
+ VALUE_DEFAULT_USE_ALL_SAMPLE_RATES);
+
+ final int singleTestIndex = mBundleFromIntent.getInt(KEY_SINGLE_TEST_INDEX,
VALUE_DEFAULT_SINGLE_TEST_INDEX);
+ // The old deprecated commands will get mapped to the closest new options.
+ final boolean shouldUseInputDevices = mBundleFromIntent.getBoolean(KEY_USE_INPUT_DEVICES,
+ VALUE_DEFAULT_USE_INPUT_CHANNEL_MASKS);
+ final boolean shouldUseInputChannelMasks =
+ mBundleFromIntent.getBoolean(KEY_USE_INPUT_CHANNEL_MASKS,
+ shouldUseInputDevices);
+
+ final boolean shouldUseOutputDevices = mBundleFromIntent.getBoolean(KEY_USE_OUTPUT_DEVICES,
+ VALUE_DEFAULT_USE_ALL_CHANNEL_COUNTS);
+ final boolean shouldUseAllChannelCounts =
+ mBundleFromIntent.getBoolean(KEY_USE_ALL_CHANNEL_COUNTS,
+ shouldUseOutputDevices);
+
+ final boolean shouldUseAllOutputChannelMasks =
+ mBundleFromIntent.getBoolean(KEY_USE_ALL_OUTPUT_CHANNEL_MASKS,
+ VALUE_DEFAULT_USE_ALL_OUTPUT_CHANNEL_MASKS);
+ final int defaultOutputChannelMasksLevel = shouldUseAllOutputChannelMasks
+ ? COVERAGE_LEVEL_ALL : COVERAGE_LEVEL_SOME;
+ final int outputChannelMasksLevel = mBundleFromIntent.getInt(KEY_OUTPUT_CHANNEL_MASKS_LEVEL,
+ defaultOutputChannelMasksLevel);
+
runOnUiThread(() -> {
mCheckBoxInputPresets.setChecked(shouldUseInputPresets);
- mCheckBoxInputDevices.setChecked(shouldUseInputDevices);
- mCheckBoxOutputDevices.setChecked(shouldUseOutputDevices);
+ mCheckBoxAllSampleRates.setChecked(shouldUseAllSampleRates);
mAutomatedTestRunner.setTestIndexText(singleTestIndex);
+ mCheckBoxAllChannels.setChecked(shouldUseAllChannelCounts);
+ mCheckBoxInputChannelMasks.setChecked(shouldUseInputChannelMasks);
+ switch(outputChannelMasksLevel) {
+ case COVERAGE_LEVEL_ALL:
+ mRadioOutputChannelMasksAll.setChecked(true);
+ break;
+ case COVERAGE_LEVEL_SOME:
+ mRadioOutputChannelMasksSome.setChecked(true);
+ break;
+ case COVERAGE_LEVEL_NONE:
+ default:
+ mRadioOutputChannelMasksNone.setChecked(true);
+ break;
+ }
});
+ // This will sync with the above checkbox code because it will log on the UI
+ // thread before running any tests.
mAutomatedTestRunner.startTest();
}
}
diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestDisconnectActivity.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestDisconnectActivity.java
index 2f3c78c5..b74c3b10 100644
--- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestDisconnectActivity.java
+++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestDisconnectActivity.java
@@ -20,9 +20,16 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
+import android.hardware.usb.UsbConstants;
+import android.hardware.usb.UsbDevice;
+import android.hardware.usb.UsbEndpoint;
+import android.hardware.usb.UsbInterface;
+import android.hardware.usb.UsbManager;
import android.os.Bundle;
+import android.util.Log;
import android.view.View;
import android.widget.Button;
+import android.widget.CheckBox;
import android.widget.TextView;
import java.io.IOException;
@@ -47,9 +54,14 @@ public class TestDisconnectActivity extends TestAudioActivity {
private volatile boolean mTestFailed;
private volatile boolean mSkipTest;
private volatile int mPlugCount;
+ private volatile int mUsbDeviceAttachedCount;
+ private volatile int mPlugState;
+ private volatile int mPlugMicrophone;
private BroadcastReceiver mPluginReceiver = new PluginBroadcastReceiver();
private Button mFailButton;
private Button mSkipButton;
+ private CheckBox mCheckBoxInputs;
+ private CheckBox mCheckBoxOutputs;
protected AutomatedTestRunner mAutomatedTestRunner;
@@ -58,15 +70,68 @@ public class TestDisconnectActivity extends TestAudioActivity {
public class PluginBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
- mPlugCount++;
+ switch (intent.getAction()) {
+ case Intent.ACTION_HEADSET_PLUG: {
+ mPlugMicrophone = intent.getIntExtra("microphone", -1);
+ mPlugState = intent.getIntExtra("state", -1);
+ mPlugCount++;
+ } break;
+ case UsbManager.ACTION_USB_DEVICE_ATTACHED:
+ case UsbManager.ACTION_USB_DEVICE_DETACHED: {
+ UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
+ final boolean hasAudioPlayback =
+ containsAudioStreamingInterface(device, UsbConstants.USB_DIR_OUT);
+ final boolean hasAudioCapture =
+ containsAudioStreamingInterface(device, UsbConstants.USB_DIR_IN);
+ if (hasAudioPlayback || hasAudioCapture) {
+ mPlugState =
+ intent.getAction() == UsbManager.ACTION_USB_DEVICE_ATTACHED ? 1 : 0;
+ mUsbDeviceAttachedCount++;
+ mPlugMicrophone = hasAudioCapture ? 1 : 0;
+ }
+ } break;
+ default:
+ break;
+ }
runOnUiThread(new Runnable() {
@Override
public void run() {
- String message = "Intent.HEADSET_PLUG #" + mPlugCount;
+ String message = "HEADSET_PLUG #" + mPlugCount
+ + ", USB_DEVICE_DE/ATTACHED #" + mUsbDeviceAttachedCount
+ + ", mic = " + mPlugMicrophone
+ + ", state = " + mPlugState;
mPlugTextView.setText(message);
+ log(message);
}
});
}
+
+ private static final int AUDIO_STREAMING_SUB_CLASS = 2;
+
+ /**
+ * Figure out if an UsbDevice contains audio input/output streaming interface or not.
+ *
+ * @param device the given UsbDevice
+ * @param direction the direction of the audio streaming interface
+ * @return true if the UsbDevice contains the audio input/output streaming interface.
+ */
+ private boolean containsAudioStreamingInterface(UsbDevice device, int direction) {
+ final int interfaceCount = device.getInterfaceCount();
+ for (int i = 0; i < interfaceCount; ++i) {
+ UsbInterface usbInterface = device.getInterface(i);
+ if (usbInterface.getInterfaceClass() != UsbConstants.USB_CLASS_AUDIO
+ && usbInterface.getInterfaceSubclass() != AUDIO_STREAMING_SUB_CLASS) {
+ continue;
+ }
+ final int endpointCount = usbInterface.getEndpointCount();
+ for (int j = 0; j < endpointCount; ++j) {
+ if (usbInterface.getEndpoint(j).getDirection() == direction) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
}
@Override
@@ -85,6 +150,9 @@ public class TestDisconnectActivity extends TestAudioActivity {
mStatusTextView = (TextView) findViewById(R.id.text_status);
mPlugTextView = (TextView) findViewById(R.id.text_plug_events);
+ mCheckBoxInputs = (CheckBox)findViewById(R.id.checkbox_disco_inputs);
+ mCheckBoxOutputs = (CheckBox)findViewById(R.id.checkbox_disco_outputs);
+
mFailButton = (Button) findViewById(R.id.button_fail);
mSkipButton = (Button) findViewById(R.id.button_skip);
updateFailSkipButton(false);
@@ -138,6 +206,8 @@ public class TestDisconnectActivity extends TestAudioActivity {
public void onResume() {
super.onResume();
IntentFilter filter = new IntentFilter(Intent.ACTION_HEADSET_PLUG);
+ filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
+ filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED);
this.registerReceiver(mPluginReceiver, filter);
}
@@ -185,15 +255,25 @@ public class TestDisconnectActivity extends TestAudioActivity {
private String getConfigText(StreamConfiguration config) {
return ((config.getDirection() == StreamConfiguration.DIRECTION_OUTPUT) ? "OUT" : "IN")
+ ", Perf = " + StreamConfiguration.convertPerformanceModeToText(
- config.getPerformanceMode())
+ config.getPerformanceMode())
+ ", " + StreamConfiguration.convertSharingModeToText(config.getSharingMode())
- + ", " + config.getSampleRate();
+ + ", " + config.getSampleRate()
+ + ", SRC = " + StreamConfiguration.convertRateConversionQualityToText(config.getRateConversionQuality());
+ }
+
+ private void log(Exception e) {
+ Log.e(TestAudioActivity.TAG, "Caught ", e);
+ mAutomatedTestRunner.log("Caught " + e);
}
private void log(String text) {
mAutomatedTestRunner.log(text);
}
+ private void flushLog() {
+ mAutomatedTestRunner.flushLog();
+ }
+
private void appendFailedSummary(String text) {
mAutomatedTestRunner.appendFailedSummary(text);
}
@@ -202,14 +282,36 @@ public class TestDisconnectActivity extends TestAudioActivity {
int perfMode,
int sharingMode,
int sampleRate,
+ int sampleRateConversionQuality,
boolean requestPlugin) throws InterruptedException {
if ((getSingleTestIndex() >= 0) && (mAutomatedTestRunner.getTestCount() != getSingleTestIndex())) {
mAutomatedTestRunner.incrementTestCount();
return;
}
+ if (!isInput && !mCheckBoxOutputs.isChecked()) {
+ return;
+ }
+ if (isInput && !mCheckBoxInputs.isChecked()) {
+ return;
+ }
+
+ updateFailSkipButton(true);
+
String actualConfigText = "none";
mSkipTest = false;
+ mTestFailed = false;
+
+ // Try to synchronize with the current headset state, IN or OUT.
+ while (mAutomatedTestRunner.isThreadEnabled() && !mSkipTest && !mTestFailed) {
+ if (requestPlugin != (mPlugState == 0)) {
+ String message = "SYNC: " + (requestPlugin ? "UNplug" : "Plug IN") + " headset now!";
+ setInstructionsText(message);
+ Thread.sleep(POLL_DURATION_MILLIS);
+ } else {
+ break;
+ }
+ }
AudioInputTester mAudioInTester = null;
AudioOutputTester mAudioOutTester = null;
@@ -234,8 +336,9 @@ public class TestDisconnectActivity extends TestAudioActivity {
requestedConfig.setPerformanceMode(perfMode);
requestedConfig.setSharingMode(sharingMode);
requestedConfig.setSampleRate(sampleRate);
+
if (sampleRate != 0) {
- requestedConfig.setRateConversionQuality(StreamConfiguration.RATE_CONVERSION_QUALITY_MEDIUM);
+ requestedConfig.setRateConversionQuality(sampleRateConversionQuality);
}
log("========================== #" + mAutomatedTestRunner.getTestCount());
@@ -246,20 +349,24 @@ public class TestDisconnectActivity extends TestAudioActivity {
Thread.sleep(SETTLING_TIME_MILLIS);
if (!mAutomatedTestRunner.isThreadEnabled()) return;
boolean openFailed = false;
+ boolean hasMicFailed = false;
AudioStreamBase stream = null;
try {
openAudio();
log("Actual:");
actualConfigText = getConfigText(actualConfig)
- + ", " + (actualConfig.isMMap() ? "MMAP" : "Legacy");
+ + ", " + ((actualConfig.isMMap() ? "MMAP" : "Legacy")
+ + ", Dev = " + actualConfig.getDeviceId()
+ );
log(actualConfigText);
+ flushLog();
stream = (isInput)
? mAudioInTester.getCurrentAudioStream()
: mAudioOutTester.getCurrentAudioStream();
} catch (IOException e) {
openFailed = true;
- log(e.getMessage());
+ log(e);
}
// The test is only worth running if we got the configuration we requested.
@@ -285,14 +392,13 @@ public class TestDisconnectActivity extends TestAudioActivity {
} catch (IOException e) {
e.printStackTrace();
valid = false;
- log(e.getMessage());
+ log(e);
}
}
int oldPlugCount = mPlugCount;
if (!openFailed && valid) {
mTestFailed = false;
- updateFailSkipButton(true);
// poll until stream started
while (!mTestFailed && mAutomatedTestRunner.isThreadEnabled() && !mSkipTest &&
stream.getState() == StreamConfiguration.STREAM_STATE_STARTING) {
@@ -305,15 +411,18 @@ public class TestDisconnectActivity extends TestAudioActivity {
// Wait for Java plug count to change or stream to disconnect.
while (!mTestFailed && mAutomatedTestRunner.isThreadEnabled() && !mSkipTest &&
stream.getState() == StreamConfiguration.STREAM_STATE_STARTED) {
+ flushLog();
Thread.sleep(POLL_DURATION_MILLIS);
if (mPlugCount > oldPlugCount) {
timeoutCount = TIME_TO_FAILURE_MILLIS / POLL_DURATION_MILLIS;
break;
}
}
+
// Wait for timeout or stream to disconnect.
while (!mTestFailed && mAutomatedTestRunner.isThreadEnabled() && !mSkipTest && (timeoutCount > 0) &&
stream.getState() == StreamConfiguration.STREAM_STATE_STARTED) {
+ flushLog();
Thread.sleep(POLL_DURATION_MILLIS);
timeoutCount--;
if (timeoutCount == 0) {
@@ -322,15 +431,26 @@ public class TestDisconnectActivity extends TestAudioActivity {
setStatusText("Plug detected by Java.\nCounting down to Oboe failure: " + timeoutCount);
}
}
- if (!mTestFailed) {
- int error = stream.getLastErrorCallbackResult();
- if (error != StreamConfiguration.ERROR_DISCONNECTED) {
- log("onEerrorCallback error = " + error
- + ", expected " + StreamConfiguration.ERROR_DISCONNECTED);
- mTestFailed = true;
+
+ if (mSkipTest) {
+ setStatusText("Skipped");
+ } else {
+ if (mTestFailed) {
+ // Check whether the peripheral has a microphone.
+ // Sometimes the microphones does not appear on the first HEADSET_PLUG event.
+ if (isInput && (mPlugMicrophone == 0)) {
+ hasMicFailed = true;
+ }
+ } else {
+ int error = stream.getLastErrorCallbackResult();
+ if (error != StreamConfiguration.ERROR_DISCONNECTED) {
+ log("onErrorCallback error = " + error
+ + ", expected " + StreamConfiguration.ERROR_DISCONNECTED);
+ mTestFailed = true;
+ }
}
+ setStatusText(mTestFailed ? "Failed" : "Passed - detected");
}
- setStatusText(mTestFailed ? "Failed" : "Passed - detected");
}
updateFailSkipButton(false);
setInstructionsText("Wait...");
@@ -352,6 +472,9 @@ public class TestDisconnectActivity extends TestAudioActivity {
boolean passed = !mTestFailed;
String resultText = requestPlugin ? "plugIN" : "UNplug";
resultText += ", " + (passed ? TEXT_PASS : TEXT_FAIL);
+ if (hasMicFailed) {
+ resultText += ", Headset has no mic!";
+ }
log(resultText);
if (!passed) {
appendFailedSummary("------ #" + mAutomatedTestRunner.getTestCount() + "\n");
@@ -365,17 +488,27 @@ public class TestDisconnectActivity extends TestAudioActivity {
} else {
log(TEXT_SKIP);
}
+ flushLog();
// Give hardware time to settle between tests.
Thread.sleep(1000);
mAutomatedTestRunner.incrementTestCount();
}
private void testConfiguration(boolean isInput, int performanceMode,
- int sharingMode, int sampleRate) throws InterruptedException {
+ int sharingMode, int sampleRate,
+ int sampleRateConversionQuality) throws InterruptedException {
boolean requestPlugin = true; // plug IN
- testConfiguration(isInput, performanceMode, sharingMode, sampleRate, requestPlugin);
+ testConfiguration(isInput, performanceMode, sharingMode, sampleRate,
+ sampleRateConversionQuality, requestPlugin);
requestPlugin = false; // UNplug
- testConfiguration(isInput, performanceMode, sharingMode, sampleRate, requestPlugin);
+ testConfiguration(isInput, performanceMode, sharingMode, sampleRate,
+ sampleRateConversionQuality, requestPlugin);
+ }
+
+ private void testConfiguration(boolean isInput, int performanceMode,
+ int sharingMode, int sampleRate) throws InterruptedException {
+ testConfiguration(isInput, performanceMode, sharingMode, sampleRate,
+ StreamConfiguration.RATE_CONVERSION_QUALITY_NONE);
}
private void testConfiguration(boolean isInput, int performanceMode,
@@ -390,25 +523,45 @@ public class TestDisconnectActivity extends TestAudioActivity {
testConfiguration(true, performanceMode, sharingMode);
}
+ private void testConfiguration(int performanceMode,
+ int sharingMode, int sampleRate,
+ int sampleRateConversionQuality) throws InterruptedException {
+ testConfiguration(false, performanceMode, sharingMode, sampleRate, sampleRateConversionQuality);
+ testConfiguration(true, performanceMode, sharingMode, sampleRate, sampleRateConversionQuality);
+ }
+
@Override
public void runTest() {
+
+ runOnUiThread(() -> keepScreenOn(true));
+
mPlugCount = 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);
- testConfiguration(StreamConfiguration.PERFORMANCE_MODE_LOW_LATENCY,
- StreamConfiguration.SHARING_MODE_SHARED);
testConfiguration(StreamConfiguration.PERFORMANCE_MODE_NONE,
StreamConfiguration.SHARING_MODE_SHARED);
+ if (NativeEngine.isMMapExclusiveSupported()){
+ testConfiguration(StreamConfiguration.PERFORMANCE_MODE_LOW_LATENCY,
+ StreamConfiguration.SHARING_MODE_EXCLUSIVE);
+ }
+ testConfiguration(StreamConfiguration.PERFORMANCE_MODE_LOW_LATENCY,
+ StreamConfiguration.SHARING_MODE_SHARED);
+ testConfiguration(StreamConfiguration.PERFORMANCE_MODE_LOW_LATENCY,
+ StreamConfiguration.SHARING_MODE_SHARED, 44100,
+ StreamConfiguration.RATE_CONVERSION_QUALITY_NONE);
+ testConfiguration(StreamConfiguration.PERFORMANCE_MODE_LOW_LATENCY,
+ StreamConfiguration.SHARING_MODE_SHARED, 44100,
+ StreamConfiguration.RATE_CONVERSION_QUALITY_MEDIUM);
} catch (InterruptedException e) {
- log(e.getMessage());
- showErrorToast(e.getMessage());
+ log("Test CANCELLED - INVALID!");
+ } catch (Exception e) {
+ log(e);
+ showErrorToast("Caught " + e);
} finally {
- setInstructionsText("Test completed.");
+ setInstructionsText("Test finished.");
updateFailSkipButton(false);
+ runOnUiThread(() -> keepScreenOn(false));
}
}
}
diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestInputActivity.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestInputActivity.java
index 7f33d803..a6c9bbd6 100644
--- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestInputActivity.java
+++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestInputActivity.java
@@ -17,17 +17,15 @@
package com.mobileer.oboetester;
import android.content.Intent;
-import android.media.audiofx.AcousticEchoCanceler;
-import android.media.audiofx.AutomaticGainControl;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Looper;
-import android.support.annotation.NonNull;
-import android.support.v4.content.FileProvider;
import android.view.View;
import android.widget.RadioButton;
+import androidx.annotation.NonNull;
+import androidx.core.content.FileProvider;
import java.io.File;
import java.io.IOException;
@@ -39,12 +37,12 @@ import java.io.IOException;
public class TestInputActivity extends TestAudioActivity {
protected AudioInputTester mAudioInputTester;
- private static final int NUM_VOLUME_BARS = 4;
+ // Note that this must match the number of volume bars defined in the layout file.
+ private static final int NUM_VOLUME_BARS = 8;
private VolumeBarView[] mVolumeBars = new VolumeBarView[NUM_VOLUME_BARS];
private InputMarginView mInputMarginView;
private int mInputMarginBursts = 0;
private WorkloadView mWorkloadView;
- private CommunicationDeviceView mCommunicationDeviceView;
public native void setMinimumFramesBeforeRead(int frames);
public native int saveWaveFile(String absolutePath);
@@ -67,6 +65,10 @@ public class TestInputActivity extends TestAudioActivity {
mVolumeBars[1] = (VolumeBarView) findViewById(R.id.volumeBar1);
mVolumeBars[2] = (VolumeBarView) findViewById(R.id.volumeBar2);
mVolumeBars[3] = (VolumeBarView) findViewById(R.id.volumeBar3);
+ mVolumeBars[4] = (VolumeBarView) findViewById(R.id.volumeBar4);
+ mVolumeBars[5] = (VolumeBarView) findViewById(R.id.volumeBar5);
+ mVolumeBars[6] = (VolumeBarView) findViewById(R.id.volumeBar6);
+ mVolumeBars[7] = (VolumeBarView) findViewById(R.id.volumeBar7);
mInputMarginView = (InputMarginView) findViewById(R.id.input_margin_view);
@@ -76,21 +78,13 @@ public class TestInputActivity extends TestAudioActivity {
mWorkloadView = (WorkloadView) findViewById(R.id.workload_view);
if (mWorkloadView != null) {
- mWorkloadView.setAudioStreamTester(mAudioInputTester);
+ mWorkloadView.setWorkloadReceiver((w) -> mAudioInputTester.setWorkload(w));
}
mCommunicationDeviceView = (CommunicationDeviceView) findViewById(R.id.comm_device_view);
}
@Override
- protected void onStop() {
- if (mCommunicationDeviceView != null) {
- mCommunicationDeviceView.cleanup();
- }
- super.onStop();
- }
-
- @Override
int getActivityType() {
return ACTIVITY_TEST_INPUT;
}
@@ -150,11 +144,6 @@ public class TestInputActivity extends TestAudioActivity {
resetVolumeBars();
}
- @Override
- protected void toastPauseError(int result) {
- showToast("Pause not implemented. Returned " + result);
- }
-
protected int saveWaveFile(File file) {
// Pass filename to native to write WAV file
int result = saveWaveFile(file.getAbsolutePath());
diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestOutputActivity.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestOutputActivity.java
index 0d5d0ce3..f2ac5ed5 100644
--- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestOutputActivity.java
+++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestOutputActivity.java
@@ -16,19 +16,18 @@
package com.mobileer.oboetester;
-import android.content.Context;
-import android.media.AudioManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
-import android.util.Log;
import android.view.View;
import android.widget.AdapterView;
import android.widget.CheckBox;
+import android.widget.SeekBar;
import android.widget.Spinner;
import android.widget.TextView;
import java.io.IOException;
+import java.util.Locale;
/**
* Test basic output.
@@ -38,7 +37,9 @@ public final class TestOutputActivity extends TestOutputActivityBase {
public static final int MAX_CHANNEL_BOXES = 16;
private CheckBox[] mChannelBoxes;
private Spinner mOutputSignalSpinner;
- protected CommunicationDeviceView mCommunicationDeviceView;
+ private TextView mVolumeTextView;
+ private SeekBar mVolumeSeekBar;
+ private CheckBox mShouldSetStreamControlByAttributes;
private class OutputSignalSpinnerListener implements android.widget.AdapterView.OnItemSelectedListener {
@Override
@@ -52,6 +53,21 @@ public final class TestOutputActivity extends TestOutputActivityBase {
}
}
+ private SeekBar.OnSeekBarChangeListener mVolumeChangeListener = new SeekBar.OnSeekBarChangeListener() {
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ setVolume(progress);
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ }
+ };
+
@Override
protected void inflateActivity() {
setContentView(R.layout.activity_test_output);
@@ -90,6 +106,12 @@ public final class TestOutputActivity extends TestOutputActivityBase {
mOutputSignalSpinner.setSelection(StreamConfiguration.NATIVE_API_UNSPECIFIED);
mCommunicationDeviceView = (CommunicationDeviceView) findViewById(R.id.comm_device_view);
+
+ mVolumeTextView = (TextView) findViewById(R.id.textVolumeSlider);
+ mVolumeSeekBar = (SeekBar) findViewById(R.id.faderVolumeSlider);
+ mVolumeSeekBar.setOnSeekBarChangeListener(mVolumeChangeListener);
+
+ mShouldSetStreamControlByAttributes = (CheckBox) findViewById(R.id.enableSetStreamControlByAttributes);
}
@Override
@@ -97,16 +119,9 @@ public final class TestOutputActivity extends TestOutputActivityBase {
return ACTIVITY_TEST_OUTPUT;
}
- @Override
- protected void onStop() {
- if (mCommunicationDeviceView != null) {
- mCommunicationDeviceView.cleanup();
- }
- super.onStop();
- }
-
public void openAudio() throws IOException {
super.openAudio();
+ mShouldSetStreamControlByAttributes.setEnabled(false);
}
private void configureChannelBoxes(int channelCount) {
@@ -116,6 +131,20 @@ public final class TestOutputActivity extends TestOutputActivityBase {
}
}
+ private void setVolume(int progress) {
+ // Convert from (0, 500) range to (-50, 0).
+ double decibels = (progress - 500) / 10.0f;
+ double amplitude = Math.pow(10.0, decibels / 20.0);
+ // When the slider is all way to the left, set a zero amplitude.
+ if (progress == 0) {
+ amplitude = 0;
+ }
+ mVolumeTextView.setText("Volume(dB): " + String.format(Locale.getDefault(), "%.1f",
+ decibels));
+ mAudioOutTester.setAmplitude((float) amplitude);
+ }
+
+
public void stopAudio() {
configureChannelBoxes(0);
mOutputSignalSpinner.setEnabled(true);
@@ -128,9 +157,16 @@ public final class TestOutputActivity extends TestOutputActivityBase {
super.pauseAudio();
}
+ public void releaseAudio() {
+ configureChannelBoxes(0);
+ mOutputSignalSpinner.setEnabled(true);
+ super.releaseAudio();
+ }
+
public void closeAudio() {
configureChannelBoxes(0);
mOutputSignalSpinner.setEnabled(true);
+ mShouldSetStreamControlByAttributes.setEnabled(true);
super.closeAudio();
}
@@ -149,6 +185,11 @@ public final class TestOutputActivity extends TestOutputActivityBase {
}
@Override
+ protected boolean shouldSetStreamControlByAttributes() {
+ return mShouldSetStreamControlByAttributes.isChecked();
+ }
+
+ @Override
public void startTestUsingBundle() {
try {
StreamConfiguration requestedOutConfig = mAudioOutTester.requestedConfiguration;
diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestOutputActivityBase.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestOutputActivityBase.java
index 5c17e928..731351b9 100644
--- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestOutputActivityBase.java
+++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestOutputActivityBase.java
@@ -26,7 +26,7 @@ abstract class TestOutputActivityBase extends TestAudioActivity {
AudioOutputTester mAudioOutTester;
private BufferSizeView mBufferSizeView;
- private WorkloadView mWorkloadView;
+ protected WorkloadView mWorkloadView;
@Override boolean isOutput() { return true; }
@@ -40,13 +40,14 @@ abstract class TestOutputActivityBase extends TestAudioActivity {
super.findAudioCommon();
mBufferSizeView = (BufferSizeView) findViewById(R.id.buffer_size_view);
mWorkloadView = (WorkloadView) findViewById(R.id.workload_view);
+ if (mWorkloadView != null) {
+ mWorkloadView.setWorkloadReceiver((w) -> mAudioOutTester.setWorkload(w));
+ }
}
@Override
public AudioOutputTester addAudioOutputTester() {
- AudioOutputTester audioOutTester = super.addAudioOutputTester();
- mWorkloadView.setAudioStreamTester(audioOutTester);
- return audioOutTester;
+ return super.addAudioOutputTester();
}
@Override
diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestPlugLatencyActivity.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestPlugLatencyActivity.java
index 3110447b..b55fa127 100644
--- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestPlugLatencyActivity.java
+++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestPlugLatencyActivity.java
@@ -40,6 +40,7 @@ import java.util.HashMap;
public class TestPlugLatencyActivity extends TestAudioActivity {
public static final int POLL_DURATION_MILLIS = 1;
+ public static final int TIMEOUT_MILLIS = 1000;
private TextView mInstructionsTextView;
private TextView mPlugTextView;
@@ -48,6 +49,7 @@ public class TestPlugLatencyActivity extends TestAudioActivity {
private AudioManager mAudioManager;
private volatile int mPlugCount = 0;
+ private long mTimeoutAtMillis;
private AudioOutputTester mAudioOutTester;
@@ -58,12 +60,17 @@ public class TestPlugLatencyActivity extends TestAudioActivity {
@Override
public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) {
boolean isBootingUp = mDevices.isEmpty();
+ AudioDeviceInfo outputDeviceInfo = null;
for (AudioDeviceInfo info : addedDevices) {
mDevices.put(info.getId(), info);
if (!isBootingUp)
{
- log("Device Added");
+ log("====== Device Added =======");
log(adiToString(info));
+ // Only process OUTPUT devices because that is what we are testing.
+ if (info.isSink()) {
+ outputDeviceInfo = info;
+ }
}
}
@@ -71,17 +78,26 @@ public class TestPlugLatencyActivity extends TestAudioActivity {
if (isBootingUp) {
log("Starting stream with existing audio devices");
}
- updateLatency(false /* wasDeviceRemoved */);
+ if (outputDeviceInfo != null) {
+ updateLatency(false /* wasDeviceRemoved */);
+ }
}
public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) {
+ AudioDeviceInfo outputDeviceInfo = null;
for (AudioDeviceInfo info : removedDevices) {
mDevices.remove(info.getId());
- log("Device Removed");
+ log("====== Device Removed =======");
log(adiToString(info));
+ // Only process OUTPUT devices because that is what we are testing.
+ if (info.isSink()) {
+ outputDeviceInfo = info;
+ }
}
- updateLatency(true /* wasDeviceRemoved */);
+ if (outputDeviceInfo != null) {
+ updateLatency(true /* wasDeviceRemoved */);
+ }
}
}
@@ -153,41 +169,79 @@ public class TestPlugLatencyActivity extends TestAudioActivity {
startAudio();
}
- private long calculateLatencyMs(boolean wasDeviceRemoved) {
+ private void setupTimeout() {
+ mTimeoutAtMillis = System.currentTimeMillis() + TIMEOUT_MILLIS;
+ }
- long startMillis = System.currentTimeMillis();
+ private void sleepOrTimeout(String message) throws InterruptedException {
+ Thread.sleep(POLL_DURATION_MILLIS);
+ if (System.currentTimeMillis() >= mTimeoutAtMillis) {
+ throw new InterruptedException(message);
+ }
+ }
+ private long calculateLatencyMs(boolean wasDeviceRemoved) {
+ long testStartMillis = System.currentTimeMillis();
+ long frameReadMillis = -1;
+ final int TIMEOUT_MAX = 100;
+ int timeout;
try {
+ long callbackMillis = -1;
if (wasDeviceRemoved && (mAudioOutTester != null)) {
+ log("Wait for error callback != 0");
// Keep querying as long as error is ok
+ setupTimeout();
while (mAudioOutTester.getLastErrorCallbackResult() == 0) {
- Thread.sleep(POLL_DURATION_MILLIS);
+ sleepOrTimeout("timed out waiting while error==0");
}
- log("Error callback at " + (System.currentTimeMillis() - startMillis) + " ms");
+ callbackMillis = System.currentTimeMillis();
+ log("Error callback at " + (callbackMillis - testStartMillis) + " ms. " +
+ "WAIT -> CALLBACK = took " + (callbackMillis - testStartMillis) + " ms");
}
closeAudio();
- log("Audio closed at " + (System.currentTimeMillis() - startMillis) + " ms");
+ long closedMillis = System.currentTimeMillis();
+ if (callbackMillis == -1) {
+ log("Audio closed at " + (closedMillis - testStartMillis) + " ms");
+ } else {
+ log("Audio closed at " + (closedMillis - testStartMillis) + " ms. " +
+ "CALLBACK -> CLOSED took " + (closedMillis - callbackMillis) + " ms");
+ }
+
clearStreamContexts();
mAudioOutTester = addAudioOutputTester();
openAudio();
- log("Audio opened at " + (System.currentTimeMillis() - startMillis) + " ms");
+ long openedMillis = System.currentTimeMillis();
+ log("Audio opened at " + (openedMillis - testStartMillis) + " ms. " +
+ "CLOSED -> OPENED took " + (openedMillis - closedMillis) + " ms");
AudioStreamBase stream = mAudioOutTester.getCurrentAudioStream();
startAudioTest();
- log("Audio starting at " + (System.currentTimeMillis() - startMillis) + " ms");
+ long startingMillis = System.currentTimeMillis();
+ log("Audio starting at " + (startingMillis - testStartMillis) + " ms. " +
+ "OPENED -> STARTING took " + (startingMillis - openedMillis) + " ms");
+
+ setupTimeout();
while (stream.getState() == StreamConfiguration.STREAM_STATE_STARTING) {
- Thread.sleep(POLL_DURATION_MILLIS);
+ sleepOrTimeout("timed out waiting while STATE_STARTING");
}
- log("Audio started at " + (System.currentTimeMillis() - startMillis) + " ms");
+ long startedMillis = System.currentTimeMillis();
+ log("Audio started at " + (startedMillis - testStartMillis) + " ms. " +
+ "STARTING -> STARTED took " + (startedMillis - startingMillis) + " ms");
+
+ setupTimeout();
while (mAudioOutTester.getFramesRead() == 0) {
- Thread.sleep(POLL_DURATION_MILLIS);
+ sleepOrTimeout("timed out waiting while framesRead()==0");
}
- log("First frame read at " + (System.currentTimeMillis() - startMillis) + " ms");
+ frameReadMillis = System.currentTimeMillis();
+ log("First frame read at " + (frameReadMillis - testStartMillis) + " ms. " +
+ "STARTED -> READ took " + (frameReadMillis - startedMillis) + " ms");
} catch (IOException | InterruptedException e) {
+ log("EXCEPTION: " + e);
e.printStackTrace();
+ closeAudio();
return -1;
}
- return System.currentTimeMillis() - startMillis;
+ return frameReadMillis - testStartMillis;
}
public static String adiToString(AudioDeviceInfo adi) {
diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestRouteDuringCallbackActivity.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestRouteDuringCallbackActivity.java
new file mode 100644
index 00000000..4dfce073
--- /dev/null
+++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestRouteDuringCallbackActivity.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.mobileer.oboetester;
+
+import static com.mobileer.oboetester.TestAudioActivity.TAG;
+
+import android.app.Activity;
+import android.content.Context;
+import android.media.AudioManager;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.Button;
+import android.widget.RadioButton;
+import android.widget.TextView;
+
+import java.util.Random;
+
+/**
+ * Try to crash in the native AAudio code by causing a routing change
+ * while playing audio. The buffer may get deleted while we are writing to it!
+ * See b/274815060
+ */
+public class TestRouteDuringCallbackActivity extends Activity {
+
+ private TextView mStatusView;
+ private MyStreamSniffer mStreamSniffer;
+ private AudioManager mAudioManager;
+ private RadioButton mOutputButton;
+ private RadioButton mInputButton;
+ private Button mStartButton;
+ private Button mStopButton;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_routing_crash);
+ mStatusView = (TextView) findViewById(R.id.text_callback_status);
+ mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
+
+ mStartButton = (Button) findViewById(R.id.button_start_test);
+ mStopButton = (Button) findViewById(R.id.button_stop_test);
+ mOutputButton = (RadioButton) findViewById(R.id.direction_output);
+ mInputButton = (RadioButton) findViewById(R.id.direction_input);
+ setButtonsEnabled(false);
+ }
+
+ public void onStartRoutingTest(View view) {
+ startRoutingTest();
+ }
+
+ public void onStopRoutingTest(View view) {
+ stopRoutingTest();
+ }
+
+ private void setButtonsEnabled(boolean running) {
+ mStartButton.setEnabled(!running);
+ mStopButton.setEnabled(running);
+ mOutputButton.setEnabled(!running);
+ mInputButton.setEnabled(!running);
+ }
+
+ // Change routing while the stream is playing.
+ // Keep trying until we crash.
+ protected class MyStreamSniffer extends Thread {
+ boolean enabled = true;
+ int routingOption = 0;
+ StringBuffer statusBuffer = new StringBuffer();
+ int loopCount;
+
+ @Override
+ public void run() {
+ routingOption = 0;
+ changeRoute(routingOption);
+ int result;
+ Random random = new Random();
+ while (enabled) {
+ loopCount++;
+ if (routingOption == 0) {
+ statusBuffer = new StringBuffer();
+ }
+ try {
+ sleep(100);
+ boolean useInput = mInputButton.isChecked();
+ result = startStream(useInput);
+ sleep(100);
+ log("-------#" + loopCount + ", " + (useInput ? "IN" : "OUT")
+ + "\nstartStream() returned " + result);
+ int sleepTimeMillis = 500 + random.nextInt(500);
+ sleep(sleepTimeMillis);
+ routingOption = (routingOption == 0) ? 1 : 0;
+ log( "changeRoute " + routingOption);
+ changeRoute(routingOption);
+ sleep(50);
+ } catch (InterruptedException e) {
+ } finally {
+ result = stopStream();
+ log("stopStream() returned " + result);
+ }
+ }
+ changeRoute(0);
+ }
+
+ // Log to screen and logcat.
+ private void log(String text) {
+ Log.d(TAG, "RoutingCrash: " + text);
+ statusBuffer.append(text + ", sleep " + getSleepTimeMicros() + " us\n");
+ showStatus(statusBuffer.toString());
+ }
+
+ // Stop the test thread.
+ void finish() {
+ enabled = false;
+ interrupt();
+ try {
+ join(2000);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ protected void showStatus(final String message) {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mStatusView.setText(message);
+ }
+ });
+ }
+
+ private void changeRoute(int option) {
+ mAudioManager.setSpeakerphoneOn(option > 0);
+ }
+
+ private native int startStream(boolean useInput);
+ private native int getSleepTimeMicros();
+ private native int stopStream();
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ stopRoutingTest();
+ }
+
+ private void startRoutingTest() {
+ stopRoutingTest();
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ setButtonsEnabled(true);
+ mStreamSniffer = new MyStreamSniffer();
+ mStreamSniffer.start();
+ }
+
+ private void stopRoutingTest() {
+ if (mStreamSniffer != null) {
+ mStreamSniffer.finish();
+ mStreamSniffer = null;
+ getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ setButtonsEnabled(false);
+ }
+ }
+}
diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/VolumeBarView.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/VolumeBarView.java
index c6a2dc24..0dc6f4df 100644
--- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/VolumeBarView.java
+++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/VolumeBarView.java
@@ -24,6 +24,8 @@ import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.View;
+import java.util.Locale;
+
/**
* Display volume in dB as a red bar.
* Display the amplitude, 0.0 to 1.0, as text in the bar.
@@ -81,7 +83,7 @@ public class VolumeBarView extends View {
float volumeWidth = ((MIN_VOLUME_DB - mVolume) / MIN_VOLUME_DB) * mCurrentWidth;
canvas.drawRect(0.0f, 0.0f, volumeWidth,
mCurrentHeight, mBarPaint);
- String text = String.format(FORMAT_AMPLITUDE, mClippedAmplitude);
+ String text = String.format(Locale.getDefault(), FORMAT_AMPLITUDE, mClippedAmplitude);
mTextPaint.getTextBounds(text, 0, text.length(), mTextBounds);
canvas.drawText(text,
20.0f,
diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/WaveformView.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/WaveformView.java
index fb50914b..4f4705c0 100644
--- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/WaveformView.java
+++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/WaveformView.java
@@ -27,6 +27,7 @@ import android.view.View;
/**
* Display an audio waveform in a custom View.
+ * Data is assumed to have a constant x-axis increment, ie. isochronous.
*/
public class WaveformView extends View {
private static final float MESSAGE_TEXT_SIZE = 80;
@@ -105,7 +106,18 @@ public class WaveformView extends View {
if (localData == null || mSampleCount == 0) {
return;
}
+
float xScale = ((float) mCurrentWidth) / (mSampleCount - 1);
+
+ // Draw cursors.
+ if (mCursors != null) {
+ for (int i = 0; i < mCursors.length; i++) {
+ float x = mCursors[i] * xScale;
+ canvas.drawLine(x, 0, x, mCurrentHeight, mCursorPaint);
+ }
+ }
+
+ // Draw waveform.
float x0 = 0.0f;
if (xScale < 1.0) {
// Draw a vertical bar for multiple samples.
@@ -135,16 +147,11 @@ public class WaveformView extends View {
y0 = y1;
}
}
- if (mCursors != null) {
- for (int i = 0; i < mCursors.length; i++) {
- float x = mCursors[i] * xScale;
- canvas.drawLine(x, 0, x, mCurrentHeight, mCursorPaint);
- }
- }
}
/**
- * Copy data into internal buffer then repaint.
+ * Copy data into internal buffer.
+ * Caller should then postInvalidate().
*/
public void setSampleData(float[] samples) {
setSampleData(samples, 0, samples.length);
@@ -170,7 +177,8 @@ public class WaveformView extends View {
}
/**
- * Copy cursor positions into internal buffer then repaint.
+ * Copy cursor positions into internal buffer.
+ * Caller should then postInvalidate().
*/
public void setCursorData(int[] cursors) {
if (cursors == null) {
diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/WorkloadView.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/WorkloadView.java
index 78d78df7..74027c2c 100644
--- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/WorkloadView.java
+++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/WorkloadView.java
@@ -18,20 +18,29 @@ package com.mobileer.oboetester;
import android.content.Context;
import android.util.AttributeSet;
+import android.util.Log;
import android.view.LayoutInflater;
import android.widget.LinearLayout;
import android.widget.SeekBar;
import android.widget.TextView;
-public class WorkloadView extends LinearLayout {
+import java.util.Locale;
- private AudioStreamTester mAudioStreamTester;
+public class WorkloadView extends LinearLayout {
protected static final int FADER_PROGRESS_MAX = 1000; // must match layout
protected TextView mTextView;
protected SeekBar mSeekBar;
+
+ private String mLabel = "Workload";
protected ExponentialTaper mExponentialTaper;
+ public interface WorkloadReceiver {
+ void setWorkload(int workload);
+ }
+
+ WorkloadReceiver mWorkloadReceiver;
+
private SeekBar.OnSeekBarChangeListener mChangeListener = new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
@@ -64,12 +73,8 @@ public class WorkloadView extends LinearLayout {
initializeViews(context);
}
- public AudioStreamTester getAudioStreamTester() {
- return mAudioStreamTester;
- }
-
- public void setAudioStreamTester(AudioStreamTester audioStreamTester) {
- mAudioStreamTester = audioStreamTester;
+ public void setWorkloadReceiver(WorkloadReceiver workloadReceiver) {
+ mWorkloadReceiver = workloadReceiver;
}
void setFaderNormalizedProgress(double fraction) {
@@ -90,15 +95,21 @@ public class WorkloadView extends LinearLayout {
mTextView = (TextView) findViewById(R.id.textWorkload);
mSeekBar = (SeekBar) findViewById(R.id.faderWorkload);
mSeekBar.setOnSeekBarChangeListener(mChangeListener);
- mExponentialTaper = new ExponentialTaper(0.0, 100.0, 10.0);
+ setRange(0.0, 100.0);
//mSeekBar.setProgress(0);
}
+ void setRange(double dMin, double dMax) {
+ mExponentialTaper = new ExponentialTaper(dMin, dMax, 10.0);
+ }
+
private void setValueByPosition(int progress) {
- double workload = mExponentialTaper.linearToExponential(
+ int workload = (int) mExponentialTaper.linearToExponential(
((double)progress) / FADER_PROGRESS_MAX);
- mAudioStreamTester.setWorkload(workload);
- mTextView.setText("Workload = " + String.format("%6.2f", workload));
+ if (mWorkloadReceiver != null) {
+ mWorkloadReceiver.setWorkload(workload);
+ }
+ mTextView.setText(getLabel() + " = " + String.format(Locale.getDefault(), "%3d", workload));
}
@Override
@@ -106,4 +117,12 @@ public class WorkloadView extends LinearLayout {
super.setEnabled(enabled);
mSeekBar.setEnabled(enabled);
}
+
+ public String getLabel() {
+ return mLabel;
+ }
+
+ public void setLabel(String label) {
+ this.mLabel = label;
+ }
}
diff --git a/apps/OboeTester/app/src/main/res/layout/activity_cold_start_latency.xml b/apps/OboeTester/app/src/main/res/layout/activity_cold_start_latency.xml
new file mode 100644
index 00000000..b4798e45
--- /dev/null
+++ b/apps/OboeTester/app/src/main/res/layout/activity_cold_start_latency.xml
@@ -0,0 +1,157 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context=".TestColdStartLatencyActivity">
+
+ <LinearLayout
+ android:id="@+id/buttonGrid"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <RadioGroup
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:checkedButton="@+id/direction_output"
+ android:orientation="horizontal">
+
+ <RadioButton
+ android:id="@+id/direction_output"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Output" />
+
+ <RadioButton
+ android:id="@+id/direction_input"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Input" />
+ </RadioGroup>
+
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <CheckBox
+ android:id="@+id/checkbox_mmap"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginRight="8sp"
+ android:text="MMAP"
+ android:checked="true" />
+
+ <CheckBox
+ android:id="@+id/checkbox_low_latency"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginRight="8sp"
+ android:text="LOW_LATENCY"
+ android:checked="true" />
+
+ <CheckBox
+ android:id="@+id/checkbox_exclusive"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginRight="8sp"
+ android:text="EXCLUSIVE"
+ android:checked="true" />
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/start_stabilize_prompt" />
+
+ <Spinner
+ android:id="@+id/spinner_start_stabilize_time"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:entries="@array/sleep_times" />
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/close_open_prompt" />
+
+ <Spinner
+ android:id="@+id/spinner_close_open_time"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:entries="@array/sleep_times" />
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/open_start_prompt" />
+
+ <Spinner
+ android:id="@+id/spinner_open_start_time"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:entries="@array/sleep_times" />
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <Button
+ android:id="@+id/button_start_test"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:backgroundTint="@color/button_tint"
+ android:onClick="onStartColdStartLatencyTest"
+ android:text="Start Test" />
+ <Button
+ android:id="@+id/button_stop_test"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:backgroundTint="@color/button_tint"
+ android:onClick="onStopColdStartLatencyTest"
+ android:text="Stop Test" />
+ </LinearLayout>
+
+ <ScrollView
+ android:id="@+id/text_log_scroller"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+ <TextView
+ android:id="@+id/text_status"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:fontFamily="monospace"
+ android:gravity="bottom"
+ android:scrollbars="vertical"
+ android:text="@string/init_status" />
+ </ScrollView>
+
+ </LinearLayout>
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/apps/OboeTester/app/src/main/res/layout/activity_data_paths.xml b/apps/OboeTester/app/src/main/res/layout/activity_data_paths.xml
index c6354008..a3f27c07 100644
--- a/apps/OboeTester/app/src/main/res/layout/activity_data_paths.xml
+++ b/apps/OboeTester/app/src/main/res/layout/activity_data_paths.xml
@@ -23,18 +23,65 @@
android:text="InPre" />
<CheckBox
- android:id="@+id/checkbox_paths_input_devices"
+ android:id="@+id/checkbox_paths_all_channels"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="true"
- android:text="InDev" />
+ android:text="AllCh" />
<CheckBox
- android:id="@+id/checkbox_paths_output_devices"
+ android:id="@+id/checkbox_paths_all_sample_rates"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:checked="true"
- android:text="OutDev" />
+ android:checked="false"
+ android:text="AllSRs" />
+
+ <CheckBox
+ android:id="@+id/checkbox_paths_in_channel_masks"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:checked="false"
+ android:text="InChMasks" />
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="OutChMasks:" />
+
+ <RadioGroup
+ android:id="@+id/group_ch_mask_options"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <RadioButton
+ android:id="@+id/radio_out_ch_masks_none"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="none"
+ android:checked="true"
+ />
+
+ <RadioButton
+ android:id="@+id/radio_out_ch_masks_some"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="some"
+ />
+
+ <RadioButton
+ android:id="@+id/radio_out_ch_masks_all"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="all"
+ />
+ </RadioGroup>
</LinearLayout>
<TextView
diff --git a/apps/OboeTester/app/src/main/res/layout/activity_dynamic_workload.xml b/apps/OboeTester/app/src/main/res/layout/activity_dynamic_workload.xml
new file mode 100644
index 00000000..2212b5bf
--- /dev/null
+++ b/apps/OboeTester/app/src/main/res/layout/activity_dynamic_workload.xml
@@ -0,0 +1,120 @@
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:paddingLeft="@dimen/activity_horizontal_margin"
+ android:paddingTop="@dimen/activity_vertical_margin"
+ android:paddingRight="@dimen/activity_horizontal_margin"
+ android:paddingBottom="@dimen/activity_vertical_margin"
+ tools:context="com.mobileer.oboetester.DynamicWorkloadActivity">
+
+ <include layout="@layout/merge_audio_simple" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:lines="1"
+ android:text="CPUs:"
+ android:textSize="18sp"
+ android:textStyle="bold" />
+
+ <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="wrap_content"
+ android:orientation="horizontal"
+ android:paddingLeft="@dimen/small_horizontal_margin"
+ android:paddingTop="@dimen/small_vertical_margin"
+ android:paddingRight="@dimen/small_horizontal_margin"
+ android:paddingBottom="@dimen/small_vertical_margin"
+ tools:context="com.mobileer.oboetester.DynamicWorkloadActivity">
+
+ <CheckBox
+ android:id="@+id/enable_perf_hint"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginRight="8sp"
+ android:text="ADPF" />
+
+ <CheckBox
+ android:id="@+id/use_alternative_adpf"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginRight="8sp"
+ android:text="Alt ADPF" />
+
+ <CheckBox
+ android:id="@+id/hear_workload"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginRight="8sp"
+ android:text="Hear Synth" />
+
+ <CheckBox
+ android:id="@+id/draw_always"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginRight="8sp"
+ android:checked="true"
+ android:text="Scroll" />
+ </LinearLayout>
+
+ <HorizontalScrollView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+
+ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+
+ android:id="@+id/affinityLayout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:paddingLeft="@dimen/small_horizontal_margin"
+ android:paddingTop="@dimen/small_vertical_margin"
+ android:paddingRight="@dimen/small_horizontal_margin"
+ android:paddingBottom="@dimen/small_vertical_margin"
+ tools:context="com.mobileer.oboetester.DynamicWorkloadActivity">
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/cpu_affinity"
+ android:textSize="18sp"
+ android:textStyle="bold" />
+ </LinearLayout>
+
+ </HorizontalScrollView>
+
+ <com.mobileer.oboetester.WorkloadView
+ android:id="@+id/dynamic_workload_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:orientation="horizontal" />
+
+ <TextView
+ android:id="@+id/resultView"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:lines="5"
+ android:text="@string/tap_help"
+ android:textSize="18sp"
+ android:textStyle="bold" />
+
+ <com.mobileer.oboetester.MultiLineChart
+ android:id="@+id/multiline_chart"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:minHeight="100dp"
+ app:backgroundColor="@color/background_warm"
+ app:exampleDimension="24sp"
+ app:exampleDrawable="@android:drawable/ic_menu_add"
+ app:exampleString="Hello, MultiLineChart"
+ app:lineColor="@color/light_blue_600" />
+</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 65cd6ea7..42ed4d59 100644
--- a/apps/OboeTester/app/src/main/res/layout/activity_echo.xml
+++ b/apps/OboeTester/app/src/main/res/layout/activity_echo.xml
@@ -6,6 +6,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fillViewport="true" >
+
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -84,6 +85,30 @@
android:gravity="center"
android:orientation="horizontal" />
+ <com.mobileer.oboetester.VolumeBarView
+ android:id="@+id/volumeBar0"
+ android:layout_marginBottom="4dp"
+ android:layout_width="fill_parent"
+ android:layout_height="20dp" />
+
+ <com.mobileer.oboetester.VolumeBarView
+ android:id="@+id/volumeBar1"
+ android:layout_marginBottom="4dp"
+ android:layout_width="fill_parent"
+ android:layout_height="20dp" />
+
+ <com.mobileer.oboetester.VolumeBarView
+ android:id="@+id/volumeBar2"
+ android:layout_marginBottom="4dp"
+ android:layout_width="fill_parent"
+ android:layout_height="20dp" />
+
+ <com.mobileer.oboetester.VolumeBarView
+ android:id="@+id/volumeBar3"
+ android:layout_marginBottom="4dp"
+ android:layout_width="fill_parent"
+ android:layout_height="20dp" />
+
<TextView
android:id="@+id/text_status"
android:layout_width="match_parent"
diff --git a/apps/OboeTester/app/src/main/res/layout/activity_error_callback.xml b/apps/OboeTester/app/src/main/res/layout/activity_error_callback.xml
index 3abc11e9..b9fb59bf 100644
--- a/apps/OboeTester/app/src/main/res/layout/activity_error_callback.xml
+++ b/apps/OboeTester/app/src/main/res/layout/activity_error_callback.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
-<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
@@ -32,4 +32,4 @@
/>
</GridLayout>
-</android.support.constraint.ConstraintLayout>
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/apps/OboeTester/app/src/main/res/layout/activity_extra_tests.xml b/apps/OboeTester/app/src/main/res/layout/activity_extra_tests.xml
index dbd0d37e..347db970 100644
--- a/apps/OboeTester/app/src/main/res/layout/activity_extra_tests.xml
+++ b/apps/OboeTester/app/src/main/res/layout/activity_extra_tests.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
-<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
@@ -12,44 +12,74 @@
android:layout_height="wrap_content"
android:columnCount="2">
- <Button
- android:id="@+id/buttonBackToMain"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_columnWeight="1"
- android:layout_gravity="fill"
- android:backgroundTint="@color/button_tint"
- android:onClick="onLaunchMainActivity"
- android:text="Back to Main" />
-
- <Button
- android:id="@+id/buttonExternalTap"
- android:layout_gravity="fill"
- android:layout_width="0dp"
- android:layout_columnWeight="1"
- android:layout_height="wrap_content"
- android:backgroundTint="@color/button_tint"
- android:onClick="onLaunchExternalTapTest"
- android:text="External Tap" />
-
- <Button
- android:id="@+id/buttonPlugLatency"
- android:layout_gravity="fill"
- android:layout_width="0dp"
- android:layout_columnWeight="1"
- android:layout_height="wrap_content"
- android:backgroundTint="@color/button_tint"
- android:onClick="onLaunchPlugLatencyTest"
- android:text="Plug Latency" />
-
- <Button
- android:id="@+id/buttonErrorCallback"
- android:layout_gravity="fill"
- android:layout_width="0dp"
- android:layout_columnWeight="1"
- android:layout_height="wrap_content"
- android:backgroundTint="@color/button_tint"
- android:onClick="onLaunchErrorCallbackTest"
- android:text="Error Callback" />
-</GridLayout>
-</android.support.constraint.ConstraintLayout>
+ <Button
+ android:id="@+id/buttonBackToMain"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_columnWeight="1"
+ android:layout_gravity="fill"
+ android:backgroundTint="@color/button_tint"
+ android:onClick="onLaunchMainActivity"
+ android:text="Back to Main" />
+
+ <Button
+ android:id="@+id/buttonExternalTap"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_columnWeight="1"
+ android:layout_gravity="fill"
+ android:backgroundTint="@color/button_tint"
+ android:onClick="onLaunchExternalTapTest"
+ android:text="External Tap" />
+
+ <Button
+ android:id="@+id/buttonPlugLatency"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_columnWeight="1"
+ android:layout_gravity="fill"
+ android:backgroundTint="@color/button_tint"
+ android:onClick="onLaunchPlugLatencyTest"
+ android:text="Plug Latency" />
+
+ <Button
+ android:id="@+id/buttonErrorCallback"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_columnWeight="1"
+ android:layout_gravity="fill"
+ android:backgroundTint="@color/button_tint"
+ android:onClick="onLaunchErrorCallbackTest"
+ android:text="Error Callback" />
+
+ <Button
+ android:id="@+id/buttonRouteDuringCallback"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_columnWeight="1"
+ android:layout_gravity="fill"
+ android:backgroundTint="@color/button_tint"
+ android:onClick="onLaunchRouteDuringCallbackTest"
+ android:text="Routing Stress" />
+
+ <Button
+ android:id="@+id/buttonDynamicWorkload"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_columnWeight="1"
+ android:layout_gravity="fill"
+ android:backgroundTint="@color/button_tint"
+ android:onClick="onLaunchDynamicWorkloadTest"
+ android:text="CPU Load" />
+
+ <Button
+ android:id="@+id/buttonColdStartLatency"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_columnWeight="1"
+ android:layout_gravity="fill"
+ android:backgroundTint="@color/button_tint"
+ android:onClick="onLaunchColdStartLatencyTest"
+ android:text="Cold Start Latency" />
+ </GridLayout>
+</androidx.constraintlayout.widget.ConstraintLayout>
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 56bafeb2..a40a91cb 100644
--- a/apps/OboeTester/app/src/main/res/layout/activity_main.xml
+++ b/apps/OboeTester/app/src/main/res/layout/activity_main.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
-<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/linearLayout"
@@ -191,33 +191,6 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/textView" />
- <LinearLayout
- android:id="@+id/layoutBluetoothSco"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:orientation="horizontal"
- app:layout_constraintStart_toStartOf="@+id/callbackSize"
- app:layout_constraintTop_toBottomOf="@+id/callbackSize"
- >
- <CheckBox
- android:id="@+id/setBluetoothScoOn"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginTop="6dp"
- android:checked="false"
- android:onClick="onStartStopBluetoothSco"
- android:text="start/stopBluetoothSco()"
- />
-
- <TextView
- android:id="@+id/textBluetoothScoStatus"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="center_vertical"
- android:layout_marginStart="12sp"
- android:text="\?" />
- </LinearLayout>
-
<CheckBox
android:id="@+id/boxEnableWorkarounds"
android:layout_width="wrap_content"
@@ -225,8 +198,8 @@
android:layout_marginTop="6dp"
android:checked="false"
android:text="enable Oboe workarounds"
- app:layout_constraintStart_toStartOf="@+id/layoutBluetoothSco"
- app:layout_constraintTop_toBottomOf="@+id/layoutBluetoothSco" />
+ app:layout_constraintStart_toStartOf="@+id/useCallback"
+ app:layout_constraintTop_toBottomOf="@+id/useCallback" />
<CheckBox
android:id="@+id/boxEnableBackground"
@@ -276,4 +249,4 @@
app:layout_constraintStart_toStartOf="@+id/deviceView"
app:layout_constraintTop_toBottomOf="@+id/deviceView" />
-</android.support.constraint.ConstraintLayout>
+</androidx.constraintlayout.widget.ConstraintLayout>
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 5798e2ea..f470e01d 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
@@ -44,6 +44,7 @@
android:orientation="horizontal" />
<LinearLayout
+ android:id="@+id/layoutGlitch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
@@ -98,16 +99,38 @@
android:id="@+id/text_status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:lines="12"
+ android:lines="14"
android:text="@string/loopback_instructions_glitch"
android:textSize="14sp"
android:textStyle="bold" />
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <CheckBox
+ android:id="@+id/boxForceGlitch"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="6dp"
+ android:checked="false"
+ android:onClick="onForceGlitchClicked"
+ android:text="Force glitches"/>
+ <CheckBox
+ android:id="@+id/boxAutoScope"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="6dp"
+ android:checked="true"
+ android:text="Auto draw"/>
+
+ </LinearLayout>
<com.mobileer.oboetester.WaveformView
android:id="@+id/waveview_audio"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
- android:minHeight="100dp"
+ android:minHeight="200dp"
/>
</LinearLayout>
</ScrollView>
diff --git a/apps/OboeTester/app/src/main/res/layout/activity_routing_crash.xml b/apps/OboeTester/app/src/main/res/layout/activity_routing_crash.xml
new file mode 100644
index 00000000..31a09dc7
--- /dev/null
+++ b/apps/OboeTester/app/src/main/res/layout/activity_routing_crash.xml
@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context=".TestRouteDuringCallbackActivity">
+
+ <LinearLayout
+ android:id="@+id/buttonGrid"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:fontFamily="monospace"
+ android:gravity="bottom"
+ android:text="@string/routing_crash_intro" />
+
+ <RadioGroup
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:checkedButton="@+id/direction_output"
+ android:orientation="horizontal">
+
+ <RadioButton
+ android:id="@+id/direction_output"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Output" />
+
+ <RadioButton
+ android:id="@+id/direction_input"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Input" />
+ </RadioGroup>
+
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <Button
+ android:id="@+id/button_start_test"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:backgroundTint="@color/button_tint"
+ android:onClick="onStartRoutingTest"
+ android:text="Start Test" />
+ <Button
+ android:id="@+id/button_stop_test"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:backgroundTint="@color/button_tint"
+ android:onClick="onStopRoutingTest"
+ android:text="Stop Test" />
+ </LinearLayout>
+
+ <TextView
+ android:id="@+id/text_callback_status"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:fontFamily="monospace"
+ android:gravity="bottom"
+ android:lines="10"
+ android:text="@string/init_status" />
+
+ </LinearLayout>
+</androidx.constraintlayout.widget.ConstraintLayout>
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 09728da8..83c50ae4 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
@@ -6,6 +6,7 @@
android:layout_height="wrap_content"
android:fillViewport="true"
tools:context="com.mobileer.oboetester.RoundTripLatencyActivity" >
+
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -92,6 +93,13 @@
android:textSize="12sp" />
</LinearLayout>
+ <com.mobileer.oboetester.CommunicationDeviceView
+ android:id="@+id/comm_device_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:orientation="horizontal" />
+
<TextView
android:id="@+id/text_status"
android:layout_width="match_parent"
diff --git a/apps/OboeTester/app/src/main/res/layout/activity_tap_to_tone.xml b/apps/OboeTester/app/src/main/res/layout/activity_tap_to_tone.xml
index 6390e79d..6a3b5a81 100644
--- a/apps/OboeTester/app/src/main/res/layout/activity_tap_to_tone.xml
+++ b/apps/OboeTester/app/src/main/res/layout/activity_tap_to_tone.xml
@@ -18,6 +18,29 @@
<include layout="@layout/merge_audio_simple"/>
+ <com.mobileer.oboetester.CommunicationDeviceView
+ android:id="@+id/comm_device_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:orientation="horizontal" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Input Device: " />
+
+ <com.mobileer.audio_device.AudioDeviceSpinner
+ android:id="@+id/input_devices_spinner"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+ </LinearLayout>
+
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
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 cc417ece..345e8204 100644
--- a/apps/OboeTester/app/src/main/res/layout/activity_test_disconnect.xml
+++ b/apps/OboeTester/app/src/main/res/layout/activity_test_disconnect.xml
@@ -24,12 +24,31 @@
android:id="@+id/text_plug_events"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:lines="1"
+ android:lines="2"
android:text="plug #"
android:textSize="18sp"
android:textStyle="bold"
/>
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <CheckBox
+ android:id="@+id/checkbox_disco_outputs"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:checked="true"
+ android:text="Outputs" />
+
+ <CheckBox
+ android:id="@+id/checkbox_disco_inputs"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:checked="true"
+ android:text="Inputs" />
+ </LinearLayout>
<LinearLayout
android:layout_width="match_parent"
diff --git a/apps/OboeTester/app/src/main/res/layout/activity_test_input.xml b/apps/OboeTester/app/src/main/res/layout/activity_test_input.xml
index 27c99eb7..2685c2ae 100644
--- a/apps/OboeTester/app/src/main/res/layout/activity_test_input.xml
+++ b/apps/OboeTester/app/src/main/res/layout/activity_test_input.xml
@@ -49,5 +49,29 @@
android:layout_marginBottom="4dp"
android:layout_width="fill_parent"
android:layout_height="20dp" />
+
+ <com.mobileer.oboetester.VolumeBarView
+ android:id="@+id/volumeBar4"
+ android:layout_marginBottom="4dp"
+ android:layout_width="fill_parent"
+ android:layout_height="20dp" />
+
+ <com.mobileer.oboetester.VolumeBarView
+ android:id="@+id/volumeBar5"
+ android:layout_marginBottom="4dp"
+ android:layout_width="fill_parent"
+ android:layout_height="20dp" />
+
+ <com.mobileer.oboetester.VolumeBarView
+ android:id="@+id/volumeBar6"
+ android:layout_marginBottom="4dp"
+ android:layout_width="fill_parent"
+ android:layout_height="20dp" />
+
+ <com.mobileer.oboetester.VolumeBarView
+ android:id="@+id/volumeBar7"
+ android:layout_marginBottom="4dp"
+ android:layout_width="fill_parent"
+ android:layout_height="20dp" />
</LinearLayout>
</ScrollView>
diff --git a/apps/OboeTester/app/src/main/res/layout/activity_test_output.xml b/apps/OboeTester/app/src/main/res/layout/activity_test_output.xml
index 639eae7c..ff38c716 100644
--- a/apps/OboeTester/app/src/main/res/layout/activity_test_output.xml
+++ b/apps/OboeTester/app/src/main/res/layout/activity_test_output.xml
@@ -153,6 +153,24 @@
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
+ android:id="@+id/textVolumeSlider"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Volume(dB): 0.0" />
+
+ <SeekBar
+ android:id="@+id/faderVolumeSlider"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:max="500"
+ android:progress="500" />
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+ <TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Signal:" />
@@ -162,6 +180,12 @@
android:layout_height="wrap_content"
android:entries="@array/output_signals"
android:prompt="@string/output_signal_prompt" />
+ <CheckBox
+ android:id="@+id/enableSetStreamControlByAttributes"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:checked="true"
+ android:text="VolumeByAttr" />
</LinearLayout>
<com.mobileer.oboetester.CommunicationDeviceView
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
index acbcb599..2060d9a3 100644
--- a/apps/OboeTester/app/src/main/res/layout/auto_test_runner.xml
+++ b/apps/OboeTester/app/src/main/res/layout/auto_test_runner.xml
@@ -15,14 +15,14 @@
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
- android:text="@string/startAudio" />
+ android:text="@string/runTest" />
<Button
android:id="@+id/button_stop"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
- android:text="@string/stopAudio" />
+ android:text="@string/cancelTest" />
<Button
android:id="@+id/button_share"
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 d4dc0634..7cf5c2e4 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
@@ -17,6 +17,7 @@
android:layout_height="match_parent">
<RadioGroup
+ android:id="@+id/bufferSizeGroup"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
diff --git a/apps/OboeTester/app/src/main/res/layout/comm_device_view.xml b/apps/OboeTester/app/src/main/res/layout/comm_device_view.xml
index 6144f8cd..bf4ed495 100644
--- a/apps/OboeTester/app/src/main/res/layout/comm_device_view.xml
+++ b/apps/OboeTester/app/src/main/res/layout/comm_device_view.xml
@@ -2,6 +2,7 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
+ android:background="#FFE8E0F0"
android:orientation="vertical">
<LinearLayout
@@ -14,16 +15,31 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="false"
- android:text="setSpeakerphoneOn" />
+ android:text="SpkrPhone" />
<TextView
- android:id="@+id/isSpeakerphoneOn"
+ android:id="@+id/spkr_status_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:text=" => ?" />
+ android:text="?" />
+
+ <CheckBox
+ android:id="@+id/setBluetoothScoOn"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:checked="false"
+ android:text="BT_SCO"
+ />
+
+ <TextView
+ android:id="@+id/sco_status_view"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="?" />
</LinearLayout>
+
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
diff --git a/apps/OboeTester/app/src/main/res/layout/merge_audio_common.xml b/apps/OboeTester/app/src/main/res/layout/merge_audio_common.xml
index cee7bed5..b63f8c3e 100644
--- a/apps/OboeTester/app/src/main/res/layout/merge_audio_common.xml
+++ b/apps/OboeTester/app/src/main/res/layout/merge_audio_common.xml
@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent" android:layout_height="match_parent">
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
<com.mobileer.oboetester.StreamConfigurationView
android:id="@+id/streamConfiguration"
@@ -31,59 +32,102 @@
<Button
android:id="@+id/button_open"
android:layout_width="0dp"
- android:layout_weight="1"
android:layout_height="wrap_content"
+ android:layout_weight="1"
android:backgroundTint="@xml/button_color_selector"
android:backgroundTintMode="src_atop"
android:onClick="openAudio"
- android:text="@string/openAudio" />
+ android:text="@string/openAudio"
+ android:textSize="12sp" />
<Button
android:id="@+id/button_start"
android:layout_width="0dp"
- android:layout_weight="1"
android:layout_height="wrap_content"
+ android:layout_weight="1"
android:backgroundTint="@xml/button_color_selector"
android:backgroundTintMode="src_atop"
android:onClick="startAudio"
- android:text="@string/startAudio" />
+ android:text="@string/startAudio"
+ android:textSize="12sp" />
<Button
android:id="@+id/button_pause"
android:layout_width="0dp"
- android:layout_weight="1"
android:layout_height="wrap_content"
+ android:layout_weight="1"
android:backgroundTint="@xml/button_color_selector"
android:backgroundTintMode="src_atop"
android:onClick="pauseAudio"
- android:text="@string/pauseAudio" />
+ android:text="@string/pauseAudio"
+ android:textSize="12sp" />
<Button
- android:id="@+id/button_stop"
+ android:id="@+id/button_flush"
android:layout_width="0dp"
+ android:layout_height="wrap_content"
android:layout_weight="1"
+ android:backgroundTint="@xml/button_color_selector"
+ android:backgroundTintMode="src_atop"
+ android:onClick="flushAudio"
+ android:text="@string/flushAudio"
+ android:textSize="12sp" />
+
+ <Button
+ android:id="@+id/button_stop"
+ android:layout_width="0dp"
android:layout_height="wrap_content"
+ android:layout_weight="1"
android:backgroundTint="@xml/button_color_selector"
android:backgroundTintMode="src_atop"
android:onClick="stopAudio"
- android:text="@string/stopAudio" />
+ android:text="@string/stopAudio"
+ android:textSize="12sp" />
<Button
- android:id="@+id/button_close"
+ android:id="@+id/button_release"
android:layout_width="0dp"
+ android:layout_height="wrap_content"
android:layout_weight="1"
+ android:backgroundTint="@xml/button_color_selector"
+ android:backgroundTintMode="src_atop"
+ android:onClick="releaseAudio"
+ android:text="@string/releaseAudio"
+ android:textSize="12sp" />
+
+ <Button
+ android:id="@+id/button_close"
+ android:layout_width="0dp"
android:layout_height="wrap_content"
+ android:layout_weight="1"
android:backgroundTint="@xml/button_color_selector"
android:backgroundTintMode="src_atop"
android:onClick="closeAudio"
- android:text="@string/closeAudio" />
-
+ android:text="@string/closeAudio"
+ android:textSize="12sp" />
</LinearLayout>
- <CheckBox
- android:id="@+id/callbackReturnStop"
- android:layout_width="wrap_content"
+ <LinearLayout
+ android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:text="callback returns STOP" />
+ android:orientation="horizontal">
+
+ <CheckBox
+ android:id="@+id/callbackReturnStop"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="callback ret STOP" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/hang_prompt" />
+
+ <Spinner
+ android:id="@+id/spinner_hang_time"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:entries="@array/hang_times" />
+ </LinearLayout>
</merge>
diff --git a/apps/OboeTester/app/src/main/res/layout/sample_multi_line_chart.xml b/apps/OboeTester/app/src/main/res/layout/sample_multi_line_chart.xml
new file mode 100644
index 00000000..67368913
--- /dev/null
+++ b/apps/OboeTester/app/src/main/res/layout/sample_multi_line_chart.xml
@@ -0,0 +1,16 @@
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <com.mobileer.oboetester.MultiLineChart
+ style="@style/Widget.AppTheme.MyView"
+ android:layout_width="300dp"
+ android:layout_height="300dp"
+ android:paddingLeft="20dp"
+ android:paddingBottom="40dp"
+ app:exampleDimension="24sp"
+ app:exampleDrawable="@android:drawable/ic_menu_add"
+ app:exampleString="Hello, MultiLineChart" />
+
+</FrameLayout> \ 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 9609fd0e..4ccfb8a1 100644
--- a/apps/OboeTester/app/src/main/res/layout/stream_config.xml
+++ b/apps/OboeTester/app/src/main/res/layout/stream_config.xml
@@ -235,6 +235,27 @@
</TableRow>
+ <TableRow>
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/capacity_prompt" />
+
+ <Spinner
+ android:id="@+id/spinnerCapacity"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:entries="@array/capacity_values" />
+
+ <TextView
+ android:id="@+id/actualCapacity"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="\?" />
+
+ </TableRow>
+
</TableLayout>
<LinearLayout
@@ -389,6 +410,7 @@
android:id="@+id/streamInfo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
+ android:lines="2"
android:text="info:" />
<TextView
diff --git a/apps/OboeTester/app/src/main/res/menu/menu_main.xml b/apps/OboeTester/app/src/main/res/menu/menu_main.xml
index d2a67eec..cc78c57b 100644
--- a/apps/OboeTester/app/src/main/res/menu/menu_main.xml
+++ b/apps/OboeTester/app/src/main/res/menu/menu_main.xml
@@ -1,5 +1,9 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:tools="http://schemas.android.com/tools" tools:context="com.mobileer.oboetester.TapToToneActivity">
- <item android:id="@+id/action_settings" android:title="@string/action_settings"
- android:orderInCategory="100" android:showAsAction="never" />
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+ <item
+ android:id="@+id/action_share"
+ android:orderInCategory="100"
+ android:title="@string/share"
+ app:showAsAction="never" />
+
</menu>
diff --git a/apps/OboeTester/app/src/main/res/values-night/styles.xml b/apps/OboeTester/app/src/main/res/values-night/styles.xml
new file mode 100644
index 00000000..35c861d9
--- /dev/null
+++ b/apps/OboeTester/app/src/main/res/values-night/styles.xml
@@ -0,0 +1,7 @@
+<resources>
+
+ <style name="Widget.AppTheme.MyView" parent="">
+ <item name="android:background">@color/gray_600</item>
+ <item name="exampleColor">@color/light_blue_600</item>
+ </style>
+</resources> \ No newline at end of file
diff --git a/apps/OboeTester/app/src/main/res/values/attrs_fast_button.xml b/apps/OboeTester/app/src/main/res/values/attrs_fast_button.xml
index 545d7ec2..e729f897 100644
--- a/apps/OboeTester/app/src/main/res/values/attrs_fast_button.xml
+++ b/apps/OboeTester/app/src/main/res/values/attrs_fast_button.xml
@@ -1,8 +1,8 @@
<resources>
<declare-styleable name="FastButton">
- <attr name="exampleString" format="string" />
- <attr name="exampleDimension" format="dimension" />
- <attr name="exampleColor" format="color" />
- <attr name="exampleDrawable" format="color|reference" />
+ <attr name="fastString" format="string" />
+ <attr name="fastDimension" format="dimension" />
+ <attr name="fastColor" format="color" />
+ <attr name="fastDrawable" format="color|reference" />
</declare-styleable>
</resources>
diff --git a/apps/OboeTester/app/src/main/res/values/attrs_multi_line_chart.xml b/apps/OboeTester/app/src/main/res/values/attrs_multi_line_chart.xml
new file mode 100644
index 00000000..2bd9b3d5
--- /dev/null
+++ b/apps/OboeTester/app/src/main/res/values/attrs_multi_line_chart.xml
@@ -0,0 +1,10 @@
+<resources>
+ <declare-styleable name="MultiLineChart">
+ <attr name="exampleString" format="string" />
+ <attr name="exampleDimension" format="dimension" />
+ <attr name="exampleColor" format="color" />
+ <attr name="lineColor" format="color" />
+ <attr name="backgroundColor" format="color" />
+ <attr name="exampleDrawable" format="color|reference" />
+ </declare-styleable>
+</resources> \ No newline at end of file
diff --git a/apps/OboeTester/app/src/main/res/values/colors.xml b/apps/OboeTester/app/src/main/res/values/colors.xml
index fbef9c51..42a8f666 100644
--- a/apps/OboeTester/app/src/main/res/values/colors.xml
+++ b/apps/OboeTester/app/src/main/res/values/colors.xml
@@ -20,4 +20,9 @@
<color name="version_draft">#ffffb0b0</color>
<color name="version_release">#fff0f0f0</color>
<color name="button_tint">#ffe0f0e0</color>
+ <color name="light_blue_400">#FF29B6F6</color>
+ <color name="light_blue_600">#FF039BE5</color>
+ <color name="gray_400">#FFBDBDBD</color>
+ <color name="gray_600">#FF757575</color>
+ <color name="background_warm">#FFf0F0E8</color>
</resources>
diff --git a/apps/OboeTester/app/src/main/res/values/dimens.xml b/apps/OboeTester/app/src/main/res/values/dimens.xml
index 4fbc15f9..8c7353d3 100644
--- a/apps/OboeTester/app/src/main/res/values/dimens.xml
+++ b/apps/OboeTester/app/src/main/res/values/dimens.xml
@@ -2,6 +2,8 @@
<!-- Default screen margins, per the Android Design guidelines. -->
<dimen name="activity_horizontal_margin">16dp</dimen>
<dimen name="activity_vertical_margin">16dp</dimen>
+ <dimen name="small_horizontal_margin">6dp</dimen>
+ <dimen name="small_vertical_margin">6dp</dimen>
<dimen name="waveform_stroke_width">2.0dp</dimen>
<dimen name="big_font_size">90dp</dimen>
diff --git a/apps/OboeTester/app/src/main/res/values/strings.xml b/apps/OboeTester/app/src/main/res/values/strings.xml
index a89ed940..da57ffe5 100644
--- a/apps/OboeTester/app/src/main/res/values/strings.xml
+++ b/apps/OboeTester/app/src/main/res/values/strings.xml
@@ -4,17 +4,21 @@
<string name="init_device">Device:</string>
<string name="init_status">Status:</string>
<string name="tap_help">Click START button!</string>
- <string name="action_settings">Settings</string>
<string name="openAudio">Open</string>
<string name="startAudio">Start</string>
<string name="pauseAudio">Pause</string>
+ <string name="flushAudio">Flush</string>
<string name="stopAudio">Stop</string>
+ <string name="releaseAudio">Rlse</string>
<string name="closeAudio">Close</string>
<string name="recordAudio">Record</string>
<string name="playAudio">Play</string>
<string name="measure">Measure</string>
<string name="cancel">Cancel</string>
<string name="clear">Clear</string>
+ <string name="clear_comm">clearCommunicationDevice()</string>
+ <string name="runTest">Run</string>
+ <string name="cancelTest">Cancel</string>
<string name="GetParam">Get Param</string>
<string name="device_name">Device Name</string>
@@ -49,6 +53,12 @@
Set volume to medium-high.\n
</string>
+ <string name="auto_glitch_instructions">
+ Plug in a loopback adapter.\n
+ Then set volume to about 80%.\n
+ Then press Run button.\n
+ </string>
+
<string name="api_prompt">Choose an API</string>
<string-array name="output_modes">
<item>Unspecified</item>
@@ -88,6 +98,7 @@
<item>PCM_FLOAT</item>
<item>PCM_I24</item>
<item>PCM_I32</item>
+ <item>IEC61937</item>
</string-array>
<string name="input_preset_prompt">InPreset:</string>
@@ -143,6 +154,22 @@
<item>13</item>
</string-array>
+ <string name="capacity_prompt">Capacity:</string>
+ <string-array name="capacity_values">
+ <item>0</item>
+ <item>32</item>
+ <item>64</item>
+ <item>128</item>
+ <item>256</item>
+ <item>384</item>
+ <item>512</item>
+ <item>1024</item>
+ <item>2048</item>
+ <item>4096</item>
+ <item>8192</item>
+ <item>16384</item>
+ </string-array>
+
<string-array name="glitch_times">
<item>10</item>
<item>25</item>
@@ -168,6 +195,24 @@
<item>White Noise</item>
</string-array>
+ <string name="hang_prompt">, Hang(ms)</string>
+ <string-array name="hang_times">
+ <item>0</item>
+ <item>1</item>
+ <item>2</item>
+ <item>3</item>
+ <item>4</item>
+ <item>5</item>
+ <item>10</item>
+ <item>20</item>
+ <item>30</item>
+ <item>100</item>
+ <item>300</item>
+ <item>1000</item>
+ <item>2000</item>
+ <item>5000</item>
+ </string-array>
+
<string name="synth_sender_text">Select Sender for Synth</string>
<string name="error_port_busy">Selected port is in use or unavailable.</string>
<string name="port_open_ok">Port opened OK.</string>
@@ -189,6 +234,13 @@
<string name="title_data_paths">Data Paths</string>
<string name="title_extra_tests">Extra Tests</string>
+ <string name="title_external_tap">External Tap To Tone</string>
+ <string name="title_plug_latency">Plug Latency Test</string>
+ <string name="title_error_callback">Error Callback Test</string>
+ <string name="title_route_during_callback">Route Callback Test</string>
+ <string name="title_dynamic_load">Dynamic CPU Load</string>
+ <string name="title_cold_start_latency">Cold Start Latency</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>
@@ -200,18 +252,26 @@
<string name="report_magic_fail">FAIL, got %X, expected %X"</string>
<string name="log_of_test_results">Log of Test Results</string>
<string name="save_file">Save</string>
+ <string name="cpu_affinity">CPUs:</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_datapath_instructions">Disconnect all headsets.\nIn quiet room, volume up, [START]</string>
- <string name="test_disconnect_instructions">Disconnect all headsets.\nPress [START]</string>
- <string name="title_external_tap">External Tap To Tone</string>
- <string name="title_plug_latency">Plug Latency Test</string>
- <string name="title_error_callback">Error Callback Test</string>
+ <string name="test_datapath_instructions">Attach peripherals\nVolume up, [START]</string>
+ <string name="test_disconnect_instructions">Disconnect all headsets.\nPress [RUN]</string>
+
<string name="analyze">Analyze</string>
+ <string name="routing_crash_intro">
+ May cause a crash by changing the\n
+ VoiceCommunication routing while playing\n
+ audio with a long duration callback.\n
+ More likely to crash with Bluetooth\n
+ headsets. Fixed in SDK 34 (U).\n
+ Issue #1763
+ </string>
+
<string-array name="conversion_qualities">
<item>None</item>
<item>Fastest</item>
@@ -223,4 +283,23 @@
<string name="channel_mask_prompt">ChannelMask:</string>
+ <string name="close_open_prompt">, Sleep before open (ms)</string>
+ <string name="open_start_prompt">, Sleep between open and start (ms)</string>
+ <string name="start_stabilize_prompt">, Sleep for start to stabilize (ms)</string>
+ <string-array name="sleep_times">
+ <item>0</item>
+ <item>4</item>
+ <item>10</item>
+ <item>20</item>
+ <item>30</item>
+ <item>100</item>
+ <item>300</item>
+ <item>1000</item>
+ <item>2000</item>
+ <item>5000</item>
+ <item>10000</item>
+ <item>20000</item>
+ <item>50000</item>
+ </string-array>
+
</resources>
diff --git a/apps/OboeTester/app/src/main/res/values/styles.xml b/apps/OboeTester/app/src/main/res/values/styles.xml
index ff6c9d2c..58dadb7f 100644
--- a/apps/OboeTester/app/src/main/res/values/styles.xml
+++ b/apps/OboeTester/app/src/main/res/values/styles.xml
@@ -5,4 +5,9 @@
<!-- Customize your theme here. -->
</style>
+ <style name="Widget.AppTheme.MyView" parent="">
+ <item name="android:background">@color/gray_400</item>
+ <item name="exampleColor">@color/light_blue_400</item>
+ </style>
+
</resources>
diff --git a/apps/OboeTester/docs/AutomatedTesting.md b/apps/OboeTester/docs/AutomatedTesting.md
index ebe4f348..e9333ac2 100644
--- a/apps/OboeTester/docs/AutomatedTesting.md
+++ b/apps/OboeTester/docs/AutomatedTesting.md
@@ -23,7 +23,6 @@ The latency, glitch and data_paths tests also need:
\* If you don't have a 3.5 mm jack then you can use a USB-C to 3.5mm adapter.
In order to use ADB at the same time you will also need a USB switching device
or use ADB/Wifi.
-Internally at Google, the [TigerTail](https://go/tigertail) device can be used for USB.
## Start App from Intent
@@ -54,7 +53,7 @@ Boolean parameters are sent using:
For example:
- --ez use_input_devices false
+ --ez use_input_presets false
## Parameters
@@ -68,7 +67,10 @@ There are two required parameters for all tests:
The "input" test will open and start an input stream.
The "output" test will open and start an output stream.
- --es file {full path for resulting file} // We recommend using the "/sdcard/Download" folder
+ --es file {name of resulting file}
+
+The file will be stored in a directory that can be written by OboeTester without any special permissions.
+This is typically "/storage/emulated/0/Android/data/com.mobileer.oboetester/files/".
There are some optional parameter in common for all tests:
@@ -89,6 +91,7 @@ There are several optional parameters in common for glitch, latency, input, and
--ei sample_rate {hertz}
--es in_perf {"none", "lowlat", "powersave"} // input performance mode, default is "lowlat"
--es out_perf {"none", "lowlat", "powersave"} // output performance mode, default is "lowlat"
+ --es out_usage {"media", "voice_communication", "alarm", "notification", "game"} // default is media
--es in_sharing {"shared", "exclusive"} // input sharing mode, default is "exclusive"
--es out_sharing {"shared", "exclusive"} // output sharing mode, default is "exclusive"
--ez in_use_mmap {"true", 1, "false", 0} // if true then MMAP is allowed, if false then MMAP will be disabled
@@ -105,13 +108,24 @@ There are several optional parameters for just the "glitch" test:
// input preset, default is "voicerec"
--es in_preset ("generic", "camcorder", "voicerec", "voicecomm", "unprocessed", "performance"}
-There are several optional parameters for just the "data_paths" test:
+There are several optional parameters for just the "data_paths" test. Note the Note the use of "-ez" for the boolean parameters.
- --ez use_input_presets {"true", 1, "false", 0} // Whether to test various input presets. Note use of "-ez"
- --ez use_input_devices {"true", 1, "false", 0} // Whether to test various input devices. Note use of "-ez"
- --ez use_output_devices {"true", 1, "false", 0} // Whether to test various output devices. Note use of "-ez"
+ --ez use_input_presets {"true", 1, "false", 0} // Whether to test various input presets.
+ --ez use_all_sample_rates {"true", 1, "false", 0} // Whether to test all sample rates. Note use of "-ez". Default is false
--ei single_test_index {testId} // Index for testing one specific test
+These parameters are used with the "data_paths" test starting with v2.5.11.
+
+ --ez use_input_channel_masks {"true", 1, "false", 0} // Whether to test the reported input channel MASKS. Default is false.
+ --ez use_all_channel_counts {"true", 1, "false", 0} // Whether to test all the supported channel COUNTS. Default is true.
+ --ei output_channel_masks_level {0, 1, 2} // Whether to test NONE=0, SOME=1, or ALL=2 channel masks. Default is false.
+
+These parameters were used with the "data_paths" test prior to v2.5.11.
+
+ --ez use_input_devices {"true", 1, "false", 0} // Whether to test various input devices.
+ --ez use_output_devices {"true", 1, "false", 0} // Whether to test various output devices.
+ --ez use_all_output_channel_masks {"true", 1, "false", 0} // Whether to test all output channel masks. Default is false
+
There are some optional parameters for just the "output" test:
--es signal_type {sine, sawtooth, freq_sweep, pitch_sweep, white_noise} // type of sound to play, default is sine
@@ -120,18 +134,18 @@ For example, a complete command for a "latency" test might be:
adb shell am start -n com.mobileer.oboetester/.MainActivity \
--es test latency \
- --es file /sdcard/Download/latency20190903.txt \
--ei buffer_bursts 2 \
--ef volume 0.8 \
--es volume_type music \
--ei buffer_bursts 2 \
- --ei out_channels 1
+ --ei out_channels 1 \
+ --es out_usage game \
+ --es file latency20230608.txt
or for a "glitch" test:
adb shell am start -n com.mobileer.oboetester/.MainActivity \
--es test glitch \
- --es file /sdcard/Download/glitch20190903.txt \
--es in_perf lowlat \
--es out_perf lowlat \
--es in_sharing exclusive \
@@ -140,49 +154,67 @@ or for a "glitch" test:
--ei sample_rate 48000 \
--ef tolerance 0.123 \
--ei in_channels 2 \
+ --es file glitch20230608.txt
or for a "data_paths" test:
adb shell am start -n com.mobileer.oboetester/.MainActivity \
--es test data_paths \
- --es file /sdcard/data_paths20190903.txt
- --ez use_input_presets true
- --ez use_input_devices false
- --ez use_output_devices true
+ --ez use_input_presets true \
+ --ez use_input_devices false \
+ --ez use_output_devices true \
+ --es file datapaths20230608.txt
## Interpreting Test Results
Test results are simple files with "name = value" pairs.
-The test results can be obtained using adb pull.
+After running the test you can determine where the results file was written by entering:
+
+ adb logcat | grep EXTFILE
- adb pull /sdcard/test20190611.txt .
+The test results can be obtained using adb pull. For example:
+
+ adb pull /storage/emulated/0/Android/data/com.mobileer.oboetester/files/glitch20230608.txt .
The beginning of the report is common to latency and glitch tests:
- build.fingerprint = google/bonito/bonito:10/QP1A.190711.017/5771233:userdebug/dev-keys
- test.version = 1.5.10
- test.version.code = 19
- time.millis = 1567521906542
- in.channels = 2
- in.perf = lowlat
- in.sharing = exclusive
- in.api = aaudio
- in.rate = 48000
- in.device = 30
- in.mmap = yes
- in.burst.frames = 96
- in.xruns = 0
- out.channels = 2
- out.perf = lowlat
- out.sharing = exclusive
- out.api = aaudio
- out.rate = 48000
- out.device = 27
- out.mmap = yes
- out.burst.frames = 96
- out.buffer.size.frames = 192
- out.buffer.capacity.frames = 3072
- out.xruns = 0
+```
+build.fingerprint = google/cheetah/cheetah:14/MASTER/eng.philbu.20230518.172104:userdebug/dev-keys
+test.version = 2.5.1
+test.version.code = 72
+time.millis = 1686156503523
+in.channels = 2
+in.perf = ll
+in.preset = voicerec
+in.sharing = ex
+in.api = aaudio
+in.rate = 48000
+in.device = 22
+in.mmap = yes
+in.rate.conversion.quality = 0
+in.hardware.channels = 2
+in.hardware.sampleRate = 48000
+in.hardware.format = i32
+in.burst.frames = 96
+in.xruns = 0
+out.channels = 1
+out.perf = ll
+out.usage = game
+out.contentType = music
+out.sharing = ex
+out.api = aaudio
+out.rate = 48000
+out.device = 3
+out.mmap = yes
+out.rate.conversion.quality = 0
+out.hardware.channels = 2
+out.hardware.sampleRate = 48000
+out.hardware.format = float
+out.burst.frames = 96
+out.buffer.size.frames = 192
+out.buffer.capacity.frames = 1920
+out.xruns = 0
+```
### Latency Report
@@ -190,18 +222,25 @@ Each test also adds specific value. For "latency". If the test fails then some v
Here is a report from a good test. The '#' comments were added for this document and are not in the report.
- rms.signal = 0.81829 # Root Mean Square of the signal, if it can be detected
- rms.noise = 0.12645 # Root Mean Square of the background noise before the signal is detected
- reset.count = 2 # number of times the full duplex stream input underflowed and had to resynchronize
- result = 0 # 0 or a negative error
- result.text = OK # text equivalent of the result
- latency.empty.frames = 476 # round trip latency if the top output buffer was empty
- latency.empty.msec = 9.92 # same but translated to milliseconds
- latency.frames = 668 # round trip latency as measured
- latency.msec = 13.92 # same but translated to milliseconds, "pro-audioo" devices should be <=20 msec
- confidence = 0.959 # quality of the latency result between 0.0 and 1.0, higher is better
-
-Here is a report from a test that failed because the output was muted. Note the latency.msec is missing because it could not be measured.
+```
+confidence = 0.892 # quality of the latency result between 0.0 and 1.0, higher is better
+result.text = OK # text equivalent of the result
+latency.msec = 23.27 # round trip latency in milliseconds
+latency.frames = 1117 # round trip latency in frames
+latency.empty.msec = 19.27 # round trip latency if the top output buffer was empty
+latency.empty.frames = 925 # same but translated to frames
+rms.signal = 0.03142 # Root Mean Square of the signal, if it can be detected
+rms.noise = 0.00262 # Root Mean Square of the background noise before the signal is detected
+correlation = 0.975 # raw normalized cross-correlation peak
+timestamp.latency.msec = 10.35 # latency based on timestamps
+timestamp.latency.mad = 0.05 # Mean absolute deviation
+timestamp.latency.count = 12 # number of measurements
+reset.count = 1 # number of times the full duplex stream input underflowed and had to resynchronize
+result = 0 # 0 or a negative error
+```
+
+Here is a report from a test that failed because the output was muted. Note the latency.msec is
+missing because it could not be measured.
rms.signal = 0.00000
rms.noise = 0.00048
@@ -227,7 +266,8 @@ Here is a report from a good test. The '#' comments were added for this document
max.time.no.glitches = 9.96 # max time with no glitches
glitch.count = 0 # number of glitch events, actual number may be higher if close together
-Here is a report from a test that failed because the output was muted. Note the glitch.count is missing because it could not be measured.
+Here is a report from a test that failed because the output was muted. Note the glitch.count is
+missing because it could not be measured.
state = WAITING_FOR_SIGNAL
unlocked.frames = 0
diff --git a/apps/OboeTester/docs/Usage.md b/apps/OboeTester/docs/Usage.md
index 050e8344..6ff69389 100644
--- a/apps/OboeTester/docs/Usage.md
+++ b/apps/OboeTester/docs/Usage.md
@@ -25,15 +25,19 @@ The latency is estimated based on the timestamp information.
Experiment with changing the buffer size to see if there are glitches when the size is high or low.
Experiment with changing the "Workload".
-Watch the frame counters and the #XRuns.
+Watch the "% CPU" and the #XRuns.
The audio is expected to glitch when the workload is high because there is too much work
and the audio task misses its delivery deadlines.
-The extra workload is generated by calculating a random number in a loop and then adding the result to the output at an inaudible level. This technique prevents the compiler optimizer from skipping the work. The workload fader is in arbitrary units that determines the number of loops.
+The extra workload is generated by calculating a number of classic synthesizer voices with two oscillators, two envelopes and a resonant filter.
You can see the affect of the changing workload in the "% cpu" report in the status area.
The extra workload will always cause glitching when you get close to 100% CPU load.
If the workload is causing glitching at a low % CPU load then there may be a problem with the callback timing.
+A variety of output signals can be played including frequency sweeps and white noise.
+The default output sound is a sine wave on each channel. The base frequency is 330 Hz
+and increases by a ratio of 4:3 (a perfect fourth) for each channel.
+
Instructions for using TEST OUTPUT for 4 or more chanels is in
[Wiki/OboeTester_MultiChannelOutput](https://github.com/google/oboe/wiki/OboeTester_MultiChannelOutput).
@@ -44,24 +48,17 @@ Test input streams. Displays current volume as VU bars.
### Tap to Tone Latency
Measure touch screen latency plus audio output latency.
-Open, Start, then tap on the screen with your fingertip.
+Press START then tap on the screen with your fingertip.
The app will listen for the sound of your fingernail tapping the screen
and the resulting beep, and then measure the time between them.
If you use headphones then you can eliminate the latency caused by speaker protection.
If you use USB-MIDI input then you can eliminate the latency due to the touch screen, which is around 15-30 msec.
MIDI latency is generally under 1 msec.
-This test works well for measuring the latency of Bluetooth headsets.
+This test works well for measuring the output latency of Bluetooth headsets.
More instructions in the [Wiki/OboeTester_TapToTone](https://github.com/google/oboe/wiki/OboeTester_TapToTone).
-### Record and Play
-
-* Tap RECORD to record several seconds of audio. You should see the red VU meters move.
-* Tap STOP then PLAY to play it back.
-* Tap SHARE button to the recorded WAV file to GDrive, GMail or another app.
-You can then examine the WAV file using a program like Audacity.
-
### Echo Input to Output
This test copies input to output and adds up to 3 seconds of delay.
@@ -90,6 +87,13 @@ The Manchaster Encoded signal provide a very sharp peak when the offset matches
Source code for the analyzer in [LatencyAnalyzer.h](https://github.com/google/oboe/blob/main/apps/OboeTester/app/src/main/cpp/analyzer/LatencyAnalyzer.h).
+### Record and Play
+
+* Tap RECORD to record several seconds of audio. You should see the red VU meters move.
+* Tap STOP then PLAY to play it back.
+* Tap SHARE button to the recorded WAV file to GDrive, GMail or another app.
+You can then examine the WAV file using a program like Audacity.
+
### Glitch Test
This test works best with a loopback adapter. But it can also work in a quiet room.
@@ -97,6 +101,15 @@ It plays a sine wave and then tries to record and lock onto that sine wave.
If the actual input does not match the expected sine wave value then it is counted as a glitch.
The test will display the maximum time that it ran without seeing a glitch.
+1. Plug in loopback adapter. (Optional)
+2. Press green bars to show input and output settings. Make changes. (optional)
+3. Press START button.
+4. Watch for state=LOCKED, which means it has detected and locked onto the sine wave output.
+5. Note the glitch count, which should be zero.
+6. Also the "max.time.no.glitches" should be as long as you run the test.
+7. Press STOP button.
+8. Press SHARE button to send a WAV file recording of the last glitch by email to yourself.
+
Look for the #XRuns display.
If #XRuns increments when a glitch occurs then the glitch is probably due to preemption of the audio task.
If #XRuns is not incrementing then the glitches may be due to AAudio MMAP tuning errors in the HAL.
@@ -113,6 +126,13 @@ The Share button will let you email the report to yourself.
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 or Drive.
+### Device Report
+
+Provides a device report. This report includes info about the features enabled, the properties set, audio
+paths, and microphones.
+You can share this report by first pressing the icon with the three vertical dots at the top right of OboeTester.
+After that, simply press the share button and you should be able to email this to yourself.
+
### Data Paths
This checks for dead speaker and mic channels, dead Input Presets and other audio data path problems.
@@ -123,3 +143,31 @@ This checks for dead speaker and mic channels, dead Input Presets and other audi
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.
+
+### External Tap-to-Tone
+
+This lets you measure the latency between touching a screen to the sound coming out on a second device.
+You could use this to measure, for example, the latency on an iPhone.
+
+1. Launch OboeTester on Android device (A) in a quiet room.
+2. Tap "EXTRAS..." button.
+3. Tap "EXTERNAL TAP" button.
+4. Launch a program on a second device (B) that will make a sound when you touch the screen. A good example would be an interactive drum pad.
+5. Tap "START" button to start listening on device (A).
+6. Tap on the app on the device (B) with your fingertip. You need to make a noise with your fingernail.
+7. Quickly tap the "ANALYSE" button on device (A).
+8. OboeTester will now look for two sounds in the recording and measure the time between them.
+
+### Plug Latency
+Measures the time it takes to close, open, and start streams as audio devices are plugged in and removed.
+
+### Error Callback Test
+Lets you delete an error callback while it is used by Oboe. Targeted test for issue #1603.
+
+### Route Callback Test
+Changes the VoiceCommunication route while playing audio. Targeted test for issue #1763.
+
+### CPU Load
+This test plays a tone and alternates between low and high workloads.
+It exercises the kernel's CPU scheduler, which controls CPU frequency and core migration.
+Moredetails on the [wiki/OboeTester_DynamicCpuLoad](https://github.com/google/oboe/wiki/OboeTester_DynamicCpuLoad).
diff --git a/apps/OboeTester/gradle.properties b/apps/OboeTester/gradle.properties
index 89e0d99e..8eebfffb 100644
--- a/apps/OboeTester/gradle.properties
+++ b/apps/OboeTester/gradle.properties
@@ -1,3 +1,20 @@
+#
+# Copyright 2023 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+#
+
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
@@ -9,10 +26,12 @@
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
-# Default value: -Xmx10248m -XX:MaxPermSize=256m
+# Default value: -Xmx1024m -XX:MaxPermSize=256m
# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
+android.useAndroidX=true
+android.enableJetifier=true
diff --git a/apps/fxlab/app/build.gradle b/apps/fxlab/app/build.gradle
index 0dbca633..52c34d81 100644
--- a/apps/fxlab/app/build.gradle
+++ b/apps/fxlab/app/build.gradle
@@ -23,7 +23,7 @@ apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
android {
- compileSdkVersion 33
+ compileSdkVersion 34
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
@@ -31,7 +31,7 @@ android {
defaultConfig {
applicationId "com.mobileer.androidfxlab"
minSdkVersion 21
- targetSdkVersion 33
+ targetSdkVersion 34
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
diff --git a/apps/fxlab/app/src/main/java/com/mobileer/androidfxlab/EffectsAdapter.kt b/apps/fxlab/app/src/main/kotlin/com/mobileer/androidfxlab/EffectsAdapter.kt
index bccefa01..bccefa01 100644
--- a/apps/fxlab/app/src/main/java/com/mobileer/androidfxlab/EffectsAdapter.kt
+++ b/apps/fxlab/app/src/main/kotlin/com/mobileer/androidfxlab/EffectsAdapter.kt
diff --git a/apps/fxlab/app/src/main/java/com/mobileer/androidfxlab/MainActivity.kt b/apps/fxlab/app/src/main/kotlin/com/mobileer/androidfxlab/MainActivity.kt
index 1873d1cf..1873d1cf 100644
--- a/apps/fxlab/app/src/main/java/com/mobileer/androidfxlab/MainActivity.kt
+++ b/apps/fxlab/app/src/main/kotlin/com/mobileer/androidfxlab/MainActivity.kt
diff --git a/apps/fxlab/app/src/main/java/com/mobileer/androidfxlab/NativeInterface.kt b/apps/fxlab/app/src/main/kotlin/com/mobileer/androidfxlab/NativeInterface.kt
index fb373b4a..fb373b4a 100644
--- a/apps/fxlab/app/src/main/java/com/mobileer/androidfxlab/NativeInterface.kt
+++ b/apps/fxlab/app/src/main/kotlin/com/mobileer/androidfxlab/NativeInterface.kt
diff --git a/apps/fxlab/app/src/main/java/com/mobileer/androidfxlab/datatype/Effect.kt b/apps/fxlab/app/src/main/kotlin/com/mobileer/androidfxlab/datatype/Effect.kt
index 7abeffc5..7abeffc5 100644
--- a/apps/fxlab/app/src/main/java/com/mobileer/androidfxlab/datatype/Effect.kt
+++ b/apps/fxlab/app/src/main/kotlin/com/mobileer/androidfxlab/datatype/Effect.kt
diff --git a/apps/fxlab/app/src/main/java/com/mobileer/androidfxlab/datatype/EffectDescription.kt b/apps/fxlab/app/src/main/kotlin/com/mobileer/androidfxlab/datatype/EffectDescription.kt
index 61f496cb..61f496cb 100644
--- a/apps/fxlab/app/src/main/java/com/mobileer/androidfxlab/datatype/EffectDescription.kt
+++ b/apps/fxlab/app/src/main/kotlin/com/mobileer/androidfxlab/datatype/EffectDescription.kt
diff --git a/apps/fxlab/app/src/main/java/com/mobileer/androidfxlab/datatype/ParamDescription.kt b/apps/fxlab/app/src/main/kotlin/com/mobileer/androidfxlab/datatype/ParamDescription.kt
index b3307704..b3307704 100644
--- a/apps/fxlab/app/src/main/java/com/mobileer/androidfxlab/datatype/ParamDescription.kt
+++ b/apps/fxlab/app/src/main/kotlin/com/mobileer/androidfxlab/datatype/ParamDescription.kt
diff --git a/build_all_android.sh b/build_all_android.sh
index 50502abe..1e5e624a 100755
--- a/build_all_android.sh
+++ b/build_all_android.sh
@@ -28,7 +28,7 @@ fi
# Directories, paths and filenames
BUILD_DIR=build
-CMAKE_ARGS="-H. \
+CMAKE_ARGS="-S. \
-DBUILD_SHARED_LIBS=true \
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
-DANDROID_TOOLCHAIN=clang \
diff --git a/docs/.nojekyll b/docs/.nojekyll
deleted file mode 100644
index e69de29b..00000000
--- a/docs/.nojekyll
+++ /dev/null
diff --git a/docs/AndroidAudioHistory.md b/docs/AndroidAudioHistory.md
index 95129edd..a10029d1 100644
--- a/docs/AndroidAudioHistory.md
+++ b/docs/AndroidAudioHistory.md
@@ -6,7 +6,7 @@ A list of important audio features, bugs, fixes and workarounds for various Andr
### 11.0 R - API 30
- Bug in **AAudio** on RQ1A (Oboe works around this issue from version 1.5 onwards). A stream is normally disconnected when a **headset is plugged in** or because of other device changes. An `AAUDIO_ERROR_DISCONNECTED` error code should be passed to the error callback. But a bug in Shared MMAP streams causes `AAUDIO_ERROR_TIMEOUT` to be returned. So if your error callback is checking for `AAUDIO_ERROR_DISCONNECTED` then it may not respond properly. We recommend **always stopping and closing the stream** regardless of the error code. Oboe does this. So if you are using Oboe callbacks you are OK. This issue was not in the original R release. It was introduced in RQ1A, which is being delivered by OTA starting in November 2020. It will be fixed in a future update. Follow it on [this public Android issue](https://issuetracker.google.com/173928197).
-- Fixed. A race condition in AudioFlinger could cause an assert in releaseBuffer() when a headset was plugged in or out. More details [here](notes/rlsbuffer.md)
+- Fixed. A race condition in AudioFlinger could cause an assert in releaseBuffer() when a headset was plugged in or out. More details [here](https://github.com/google/oboe/wiki/TechNote_ReleaseBuffer)
### 10.0 Q - API 29
- Fixed: Setting capacity of Legacy input streams < 4096 can prevent use of FAST path. https://github.com/google/oboe/issues/183. Also fixed in AAudio with ag/7116429
@@ -15,7 +15,7 @@ A list of important audio features, bugs, fixes and workarounds for various Andr
### 9.0 Pie - API 28 (August 6, 2018)
- AAudio adds support for setUsage(), setSessionId(), setContentType(), setInputPreset() for builders.
-- Regression bug: [AAudio] Headphone disconnect event not fired for MMAP streams. Issue [#252](https://github.com/google/oboe/issues/252) Also see tech note [Disconnected Streams](notes/disconnect.md).
+- Regression bug: [AAudio] Headphone disconnect event not fired for MMAP streams. Issue [#252](https://github.com/google/oboe/issues/252) Also see tech note [Disconnected Streams](https://github.com/google/oboe/wiki/TechNote_Disconnect).
- AAudio input streams with LOW_LATENCY will open a FAST path using INT16 and convert the data to FLOAT if needed. See: https://github.com/google/oboe/issues/276
### 8.1 Oreo MR1 - API 27
diff --git a/docs/ChangeLog.md b/docs/ChangeLog.md
deleted file mode 100644
index d206afa3..00000000
--- a/docs/ChangeLog.md
+++ /dev/null
@@ -1,30 +0,0 @@
-# Changelog
-
-**This changelog is deprecated**. See the [Oboe releases page](https://github.com/google/oboe/releases) for the contents of each release.
-
-## [1.0.0](https://github.com/google/oboe/releases/tag/1.0.0)
-#### 2nd October 2018
-First production ready version
-
-**API changes**
-- [Remove `AudioStream::setNativeFormat`](https://github.com/google/oboe/pull/213/commits/0e8af6a65efef55ec180f8ce76e699adcee5f413)
-- [Remove `AudioStream::isPlaying`](https://github.com/google/oboe/pull/213/commits/6437f5aa224330fbdf77ecc161cc868be663a974).
-- [Add `AudioStream::getTimestamp(clockid_t)`](https://github.com/google/oboe/pull/213/commits/ab695c116e5f196e57560a86efa3c982360838d3).
-- Deprecate `AudioStream::getTimestamp(clockid_t, int64_t, int64_t)`. Same commit as above.
-- [Add Android P functions](https://github.com/google/oboe/commit/c30bbe603c256f92cdf2876c3122bc5be24b5e3e)
-
-**Other changes**
-- Add [API reference](https://google.github.io/oboe/)
-- Add unit tests
-
-## 0.11
-#### 13th June 2018
-Change `AudioStream` method return types to `ResultWithValue` where appropriate. [Full details](https://github.com/google/oboe/pull/109).
-
-## 0.10
-#### 18th January 2018
-Add support for input (recording) streams
-
-## 0.9
-#### 18th October 2017
-Initial developer preview
diff --git a/docs/OpenSLESMigration.md b/docs/OpenSLESMigration.md
index 9f642338..5846b27e 100644
--- a/docs/OpenSLESMigration.md
+++ b/docs/OpenSLESMigration.md
@@ -153,7 +153,7 @@ Oboe does **not** support the following features:
* Channel masks - only [indexed channel masks](https://developer.android.com/reference/kotlin/android/media/AudioFormat#channel-index-masks) are supported.
* Playing audio content from a file pathname or [URI](https://en.wikipedia.org/wiki/Uniform_Resource_Identifier).
* Notification callbacks for position updates.
-* Platform output effects on API 27 and below. [They are supported from API 28 and above.](https://github.com/google/oboe/blob/main/docs/notes/effects.md)
+* Platform output effects on API 27 and below. [They are supported from API 28 and above.](https://github.com/google/oboe/wiki/TechNote_Effects)
# Summary
diff --git a/docs/README.md b/docs/README.md
index 6e7853d6..33ce62c8 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -3,11 +3,9 @@ Oboe documentation
- [Android Audio History](AndroidAudioHistory.md)
- [API reference](https://google.github.io/oboe/)
- [Apps using Oboe](AppsUsingOboe.md)
-- [Changelog](ChangeLog.md)
- [FAQs](FAQ.md)
- [Full Guide to Oboe](FullGuide.md)
- [Getting Started with Oboe](GettingStarted.md)
-- [Tech Notes](notes/)
- - [Using Audio Effects with Oboe](notes/effects.md)
- - [Disconnected Streams](notes/disconnect.md)
- [Privacy Policy](PrivacyPolicy.md)
+- [Releases](https://github.com/google/oboe/releases)
+- [Wiki](https://github.com/google/oboe/wiki)
diff --git a/docs/notes/README.md b/docs/notes/README.md
deleted file mode 100644
index db6e1315..00000000
--- a/docs/notes/README.md
+++ /dev/null
@@ -1,11 +0,0 @@
-[Oboe Docs Home](https://github.com/google/oboe/blob/main/docs/README.md)
-
-Note that the Tech Notes are moving to here: https://github.com/google/oboe/wiki#tech-notes
-
-# Oboe Tech Notes
-
-* [Buffer Terminology - Frames, Capacity, Size, Burst](https://github.com/google/oboe/wiki/TechNote_BufferTerminology)
-* [Using Audio Effects with Oboe](effects.md)
-* [Disconnected Streams](disconnect.md) - Responding to Plugging In and Unplugging Headsets
-* [Assert in releaseBuffer()](rlsbuffer.md)
-* [Glitches and Latency](https://github.com/google/oboe/wiki/TechNote_Glitches)
diff --git a/docs/notes/disconnect.md b/docs/notes/disconnect.md
deleted file mode 100644
index 26bab982..00000000
--- a/docs/notes/disconnect.md
+++ /dev/null
@@ -1,76 +0,0 @@
-[Oboe Docs Home](README.md)
-
-# Tech Note: Disconnected Streams and Plugin Issues
-
-When Oboe is using **OpenSL ES**, and a headset is plugged in or out, then OpenSL ES will automatically switch between devices.
-This is convenient but can cause problems because the new device may have different burst sizes and different latency.
-
-When Oboe is using **AAudio**, and a headset is plugged in or out, then
-the stream is no longer available and becomes "disconnected".
-The app should then be notified in one of two ways.
-
-1) If the app is using an error callback then the AudioStreamErrorCallback methods will be called.
-It will launch a thread, which will call onErrorBeforeClose().
-Then it stops and closes the stream.
-Then onErrorAfterClose() will be called.
-An app may choose to reopen a stream in the onErrorAfterClose() method.
-
-2) If an app is using read()/write() calls then they will return an error when a disconnect occurs.
-The app should then stop() and close() the stream.
-An app may then choose to reopen a stream.
-
-## Bug in P and Q
-
-On some versions of Android P (9), and some early versions of Q (10), the disconnect message does not reach AAudio and the app will not
-know that the device has changed. There is a "TEST DISCONNECT" option in
-[OboeTester](https://github.com/google/oboe/tree/main/apps/OboeTester/docs)
-that can be used to diagnose this problem.
-
-## Workaround for not Disconnecting Properly
-
-As a workaround you can listen for a Java [Intent.ACTION_HEADSET_PLUG](https://developer.android.com/reference/android/content/Intent#ACTION_HEADSET_PLUG),
-which is fired when a head set is plugged in or out. If your min SDK is LOLLIPOP or later then you can use [AudioManager.ACTION_HEADSET_PLUG](https://developer.android.com/reference/android/media/AudioManager#ACTION_HEADSET_PLUG) instead.
-
- // Receive a broadcast Intent when a headset is plugged in or unplugged.
- public class PluginBroadcastReceiver extends BroadcastReceiver {
- @Override
- public void onReceive(Context context, Intent intent) {
- // Close the stream if it was not disconnected.
- }
- }
-
- private BroadcastReceiver mPluginReceiver = new PluginBroadcastReceiver();
-
-You can register for the Intent when your app resumes and unregister when it pauses.
-
- @Override
- public void onResume() {
- super.onResume();
- IntentFilter filter = new IntentFilter(Intent.ACTION_HEADSET_PLUG);
- this.registerReceiver(mPluginReceiver, filter);
- }
-
- @Override
- public void onPause() {
- this.unregisterReceiver(mPluginReceiver);
- super.onPause();
- }
-
-## Internal Notes
-
-* Oboe Issues
- * [#381](https://github.com/google/oboe/issues/381) Connecting headphones does not trigger any event. S9
- * [#893](https://github.com/google/oboe/issues/893) onErrorBeforeClose and onErrorAfterClose not called, S10
- * [#908](https://github.com/google/oboe/issues/908) Huawei MAR-LX3A
- * [#1350](https://github.com/google/oboe/issues/1350) SM-G977B fails "Test Disconnects"
-* This issue is tracked internally as b/111711159.
-* A fix in AOSP is [here](https://android-review.googlesource.com/c/platform/frameworks/av/+/836184)
-* A fix was also merged into pi-dev on July 30, 2018.
-
-### Results from Test Disconnect in OboeTester
-
-| Device | Build | Result |
-|:--|:--|:--|
-| Pixel 1 | QQ1A.190919.002 | ALL PASS |
-| Samsung S10e | PPR1.180610.011 | MMAP Output plugIN failed |
-| Huawei MAR-LX3A | 10.0.0.216 | MMAP In/Out fails, Legacy In fails. |
diff --git a/docs/notes/effects.md b/docs/notes/effects.md
deleted file mode 100644
index 1f7d5f76..00000000
--- a/docs/notes/effects.md
+++ /dev/null
@@ -1,48 +0,0 @@
-[Tech Notes Home](README.md)
-
-# Using Audio Effects with Oboe
-
-## Overview
-
-The Android Audio framework provides some effects processing that can be used by apps.
-It is available through the Java or Kotlin
-[AudioEffect API](https://developer.android.com/reference/android/media/audiofx/AudioEffect)
-
-Another alternative is to do your own effects processing in your own app.
-
-### Reasons to use the Android AudioEffect in the OS:
-1. Functions are provided for you so they are easy to use.
-
-### Reasons to do your own effects Processing:
-1. They will work on all versions of Android. The AudioEffects can only be used with Oboe on Android 9 (Pie) and above. They are not supported for OpenSL ES.
-2. You can customize the effects as needed.
-3. You can get lower latency when you use your own effects. Using Android AudioEffects prevents you from getting a low latency path.
-
-## Using Android AudioEffects
-
-Oboe streams on Android 9 (Pie) and above can use the Java/Kotlin.
-See [AudioEffect API](https://developer.android.com/reference/android/media/audiofx/AudioEffect)
-
-The basic idea is to use Java or Kotlin to create a Session with Effects.
-Then associate your Oboe streams with the session by creating them with a SessionID.
-
-In Java:
-
- AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
- int audioSessionId = audioManager.generateAudioSessionId();
-
-Pass the audioSessionId to your C++ code using JNI. Then use it when opening your Oboe streams:
-
- builder->setSessionId((oboe::SessionId) audioSessionId);
-
-Note that these streams will probably not have low latency. So you may want to do your own effects processing.
-
-## Using Third Party Affects Processing
-
-There are many options for finding audio effects.
-
-- [Music DSP Archive](http://www.musicdsp.org/en/latest/Effects/index.html)
-- [Synthesis Toolkit in C++ (STK)](https://ccrma.stanford.edu/software/stk/index.html)
-- [Cookbook for Biquad Filters, EQ, etc.](https://www.w3.org/2011/audio/audio-eq-cookbook.html)
-- [Faust - language for generating effects, big library](https://faust.grame.fr/index.html)
-- [DAFX Digital Audio Effects conference proceedings](http://dafx.de/)
diff --git a/docs/notes/rlsbuffer.md b/docs/notes/rlsbuffer.md
deleted file mode 100644
index ff30819f..00000000
--- a/docs/notes/rlsbuffer.md
+++ /dev/null
@@ -1,71 +0,0 @@
-[Tech Notes Home](README.md)
-
-# Assert in releaseBuffer()
-
-There is a bug that can sometimes cause an assert in ClientProxy or AudioTrackShared::releaseBuffer() when headsets are connected or disconnected.
-The bug was originally reported at: https://github.com/google/oboe/issues/535
-
-You will see signatures like this in the logcat:
-
- F AudioTrackShared: releaseBuffer: mUnreleased out of range, !(stepCount:96 <= mUnreleased:0 <= mFrameCount:480), BufferSizeInFrames:480
-
-## Platforms Affected
-
-Android version 10 (Q) or earlier.
-
-Oboe version 1.4.0 or earlier when using OpenSLES with an OUTPUT stream callback.
-
-OR any version of Oboe if:
-* Oboe is using using OpenSL ES or a non-MMAP Legacy AAudio stream
-* AND you call stream->getFramesRead() or stream->getTimestamp(...) from inside
-an OUTPUT stream callback,
-
-It does **not** happen when Oboe uses AAudio MMAP because it does not call releaseBuffer().
-
-## Workarounds
-
-1. Use Oboe 1.4.1 or later.
-1. Do not call stream->getFramesRead() or stream->getTimestamp() from inside the callback of an OUTPUT stream. If you absolutely must, then call them at the beginning of your callback to reduce the probability of a crash.
-
-Here is a [fix in Oboe 1.4.1](https://github.com/google/oboe/pull/863) that removed a call to getPosition().
-
-## Root Cause
-
-The sequence of events is:
-1. AudioFlinger AudioTrack obtains a buffer from the audio device
-1. user plugs in headphones, which invalidates the audio device
-1. app is called (callback) to render audio using the buffer
-1. the app or Oboe calls getFramesRead() or getTimestamp(), which calls down to AudioTrack::getPosition() or AudioTrack::getTimestamp()
-1. device routing change occurs because the audio device is [invalid](https://cs.android.com/android/platform/superproject/+/primary:frameworks/av/media/libaudioclient/AudioTrack.cpp;l=1239;drc=48e98cf8dbd9fa212a0e129822929dc40e6c3898)
-1. callback ends by releasing the buffer back to a different device
-1. AudioTrackShared::releaseBuffer() checks to make sure the device matches the one in ObtainBuffer() and asserts if they do not match.
-
-Oboe, before V1.4.1, would update the server position in its callback. This called getPosition() in OpenSL ES, which called AudioTrack::getPosition().
-
-The probability of the assert() is proportional to the time that the CPU spends between obtaining a buffer and calling restoreTrack_l().
-
-This bug is tracked internally at: b/136268149
-
-## Reproduce the Bug
-
-These steps will trigger the bug most of the time:
-
-1. Install OboeTester 1.5.22 or later, with Oboe < 1.4.1
-1. Enter in a Terminal window: adb logcat | grep releaseBuffer
-1. Launch OboeTester
-1. Click TEST OUTPUT
-1. Select API: OpenSL ES
-1. Click OPEN
-1. Click START, you should hear a tone
-1. Slide "Workload" fader slowly up until you hear bad glitches.
-1. Plug in headphones.
-
-You may see a message like this in the logcat:
-
- AudioTrackShared: releaseBuffer: mUnreleased out of range, !(stepCount:96 <= mUnreleased:0 <= mFrameCount:480), BufferSizeInFrames:480
-
-# OEM Information
-
-These patches are available in Q AOSP:
-1. [AudioTrack](https://android-review.googlesource.com/c/platform/frameworks/av/+/1251871/)
-1. [AudioRecord](https://android-review.googlesource.com/c/platform/frameworks/av/+/1251872/)
diff --git a/include/oboe/AudioStream.h b/include/oboe/AudioStream.h
index cbec76e0..817099d5 100644
--- a/include/oboe/AudioStream.h
+++ b/include/oboe/AudioStream.h
@@ -69,6 +69,29 @@ public:
}
/**
+ * Free the audio resources associated with a stream created by AAudioStreamBuilder_openStream().
+ *
+ * AAudioStream_close() should be called at some point after calling this function.
+ *
+ * After this call, the stream will be in AAUDIO_STREAM_STATE_CLOSING
+ *
+ * This function is useful if you want to release the audio resources immediately, but still allow
+ * queries to the stream to occur from other threads. This often happens if you are monitoring
+ * stream progress from a UI thread.
+ *
+ * NOTE: This function is only fully implemented for MMAP streams, which are low latency streams
+ * supported by some devices. On other "Legacy" streams some audio resources will still be in use
+ * and some callbacks may still be in process after this call.
+ *
+ * Available in AAudio since API level 30. Returns Result::ErrorUnimplemented otherwise.
+ *
+ * * @return either Result::OK or an error.
+ */
+ virtual Result release() {
+ return Result::ErrorUnimplemented;
+ }
+
+ /**
* Close the stream and deallocate any resources from the open() call.
*/
virtual Result close();
@@ -171,6 +194,13 @@ public:
*
* This cannot be set higher than getBufferCapacity().
*
+ * This should only be used with Output streams. It will
+ * be ignored for Input streams because they are generally kept as empty as possible.
+ *
+ * For OpenSL ES, this method only has an effect on output stream that do NOT
+ * use a callback. The blocking writes goes into a buffer in Oboe and the size of that
+ * buffer is controlled by this method.
+ *
* @param requestedFrames requested number of frames that can be filled without blocking
* @return the resulting buffer size in frames (obtained using value()) or an error (obtained
* using error())
@@ -264,7 +294,7 @@ public:
*
* Note that due to issues in Android before R, we recommend NOT calling
* this method from a data callback. See this tech note for more details.
- * https://github.com/google/oboe/blob/main/docs/notes/rlsbuffer.md
+ * https://github.com/google/oboe/wiki/TechNote_ReleaseBuffer
*
* @return a ResultWithValue which has a result of Result::OK and a value containing the latency
* in milliseconds, or a result of Result::Error*.
@@ -286,7 +316,7 @@ public:
*
* Note that due to issues in Android before R, we recommend NOT calling
* this method from a data callback. See this tech note for more details.
- * https://github.com/google/oboe/blob/main/docs/notes/rlsbuffer.md
+ * https://github.com/google/oboe/wiki/TechNote_ReleaseBuffer
*
* @deprecated since 1.0, use AudioStream::getTimestamp(clockid_t clockId) instead, which
* returns ResultWithValue
@@ -313,7 +343,7 @@ public:
*
* Note that due to issues in Android before R, we recommend NOT calling
* this method from a data callback. See this tech note for more details.
- * https://github.com/google/oboe/blob/main/docs/notes/rlsbuffer.md
+ * https://github.com/google/oboe/wiki/TechNote_ReleaseBuffer
*
* See
* @param clockId the type of clock to use e.g. CLOCK_MONOTONIC
@@ -477,6 +507,43 @@ public:
mDelayBeforeCloseMillis = delayBeforeCloseMillis;
}
+ /**
+ * Enable or disable a device specific CPU performance hint.
+ * Runtime benchmarks such as the callback duration may be used to
+ * speed up the CPU and improve real-time performance.
+ *
+ * Note that this feature is device specific and may not be implemented.
+ * Also the benefits may vary by device.
+ *
+ * The flag will be checked in the Oboe data callback. If it transitions from false to true
+ * then the PerformanceHint feature will be started.
+ * This only needs to be called once.
+ *
+ * You may want to enable this if you have a dynamically changing workload
+ * and you notice that you are getting underruns and glitches when your workload increases.
+ * This might happen, for example, if you suddenly go from playing one note to
+ * ten notes on a synthesizer.
+ *
+ * Try the CPU Load test in OboeTester if you would like to experiment with this interactively.
+ *
+ * On some devices, this may be implemented using the "ADPF" library.
+ *
+ * @param enabled true if you would like a performance boost
+ */
+ void setPerformanceHintEnabled(bool enabled) {
+ mPerformanceHintEnabled = enabled;
+ }
+
+ /**
+ * This only tells you if the feature has been requested.
+ * It does not tell you if the PerformanceHint feature is implemented or active on the device.
+ *
+ * @return true if set using setPerformanceHintEnabled().
+ */
+ bool isPerformanceHintEnabled() {
+ return mPerformanceHintEnabled;
+ }
+
protected:
/**
@@ -553,6 +620,22 @@ protected:
}
}
+ /**
+ * This may be called internally at the beginning of a callback.
+ */
+ virtual void beginPerformanceHintInCallback() {}
+
+ /**
+ * This may be called internally at the end of a callback.
+ * @param numFrames passed to the callback
+ */
+ virtual void endPerformanceHintInCallback(int32_t /*numFrames*/) {}
+
+ /**
+ * This will be called when the stream is closed just in case performance hints were enabled.
+ */
+ virtual void closePerformanceHint() {}
+
/*
* Set a weak_ptr to this stream from the shared_ptr so that we can
* later use a shared_ptr in the error callback.
@@ -609,6 +692,8 @@ private:
std::atomic<bool> mDataCallbackEnabled{false};
std::atomic<bool> mErrorCallbackCalled{false};
+
+ std::atomic<bool> mPerformanceHintEnabled{false}; // set only by app
};
/**
diff --git a/include/oboe/AudioStreamBase.h b/include/oboe/AudioStreamBase.h
index e17d5397..6222e448 100644
--- a/include/oboe/AudioStreamBase.h
+++ b/include/oboe/AudioStreamBase.h
@@ -158,6 +158,41 @@ public:
SessionId getSessionId() const { return mSessionId; }
/**
+ * @return whether the content of the stream is spatialized.
+ */
+ bool isContentSpatialized() const { return mIsContentSpatialized; }
+
+ /**
+ * @return the spatialization behavior for the stream.
+ */
+ SpatializationBehavior getSpatializationBehavior() const { return mSpatializationBehavior; }
+
+ /**
+ * Return the policy that determines whether the audio may or may not be captured
+ * by other apps or the system.
+ *
+ * See AudioStreamBuilder_setAllowedCapturePolicy().
+ *
+ * Added in API level 29 to AAudio.
+ *
+ * @return the allowed capture policy, for example AllowedCapturePolicy::All
+ */
+ AllowedCapturePolicy getAllowedCapturePolicy() const { return mAllowedCapturePolicy; }
+
+ /**
+ * Return whether this input stream is marked as privacy sensitive.
+ *
+ * See AudioStreamBuilder_setPrivacySensitiveMode().
+ *
+ * Added in API level 30 to AAudio.
+ *
+ * @return PrivacySensitiveMode::Enabled if privacy sensitive,
+ * PrivacySensitiveMode::Disabled if not privacy sensitive, and
+ * PrivacySensitiveMode::Unspecified if API is not supported.
+ */
+ PrivacySensitiveMode getPrivacySensitiveMode() const { return mPrivacySensitiveMode; }
+
+ /**
* @return true if Oboe can convert channel counts to achieve optimal results.
*/
bool isChannelConversionAllowed() const {
@@ -185,6 +220,21 @@ public:
return mChannelMask;
}
+ /**
+ * @return number of channels for the hardware, for example 2 for stereo, or kUnspecified.
+ */
+ int32_t getHardwareChannelCount() const { return mHardwareChannelCount; }
+
+ /**
+ * @return hardware sample rate for the stream or kUnspecified
+ */
+ int32_t getHardwareSampleRate() const { return mHardwareSampleRate; }
+
+ /**
+ * @return the audio sample format of the hardware (e.g. Float or I16)
+ */
+ AudioFormat getHardwareFormat() const { return mHardwareFormat; }
+
protected:
/** The callback which will be fired when new data is ready to be read/written. **/
AudioStreamDataCallback *mDataCallback = nullptr;
@@ -229,11 +279,29 @@ protected:
/** Stream session ID allocation strategy. Only active on Android 28+ */
SessionId mSessionId = SessionId::None;
+ /** Allowed Capture Policy. Only active on Android 29+ */
+ AllowedCapturePolicy mAllowedCapturePolicy = AllowedCapturePolicy::Unspecified;
+
+ /** Privacy Sensitive Mode. Only active on Android 30+ */
+ PrivacySensitiveMode mPrivacySensitiveMode = PrivacySensitiveMode::Unspecified;
+
/** Control the name of the package creating the stream. Only active on Android 31+ */
std::string mPackageName;
/** Control the attribution tag of the context creating the stream. Only active on Android 31+ */
std::string mAttributionTag;
+ /** Whether the content is already spatialized. Only used on Android 32+ */
+ bool mIsContentSpatialized = false;
+ /** Spatialization Behavior. Only active on Android 32+ */
+ SpatializationBehavior mSpatializationBehavior = SpatializationBehavior::Unspecified;
+
+ /** Hardware channel count. Only specified on Android 34+ AAudio streams */
+ int32_t mHardwareChannelCount = kUnspecified;
+ /** Hardware sample rate. Only specified on Android 34+ AAudio streams */
+ int32_t mHardwareSampleRate = kUnspecified;
+ /** Hardware format. Only specified on Android 34+ AAudio streams */
+ AudioFormat mHardwareFormat = AudioFormat::Unspecified;
+
// Control whether Oboe can convert channel counts to achieve optimal results.
bool mChannelConversionAllowed = false;
// Control whether Oboe can convert data formats to achieve optimal results.
@@ -249,6 +317,7 @@ protected:
case AudioFormat::Float:
case AudioFormat::I24:
case AudioFormat::I32:
+ case AudioFormat::IEC61937:
break;
default:
diff --git a/include/oboe/AudioStreamBuilder.h b/include/oboe/AudioStreamBuilder.h
index 5c7ac4b6..1574a398 100644
--- a/include/oboe/AudioStreamBuilder.h
+++ b/include/oboe/AudioStreamBuilder.h
@@ -349,6 +349,84 @@ public:
}
/**
+ * Specify whether this stream audio may or may not be captured by other apps or the system.
+ *
+ * The default is AllowedCapturePolicy::Unspecified which maps to AAUDIO_ALLOW_CAPTURE_BY_ALL.
+ *
+ * Note that an application can also set its global policy, in which case the most restrictive
+ * policy is always applied. See android.media.AudioAttributes.setAllowedCapturePolicy.
+ *
+ * Added in API level 29 to AAudio.
+ *
+ * @param inputPreset the desired level of opt-out from being captured.
+ * @return pointer to the builder so calls can be chained
+ */
+ AudioStreamBuilder *setAllowedCapturePolicy(AllowedCapturePolicy allowedCapturePolicy) {
+ mAllowedCapturePolicy = allowedCapturePolicy;
+ return this;
+ }
+
+ /** Indicates whether this input stream must be marked as privacy sensitive or not.
+ *
+ * When PrivacySensitiveMode::Enabled, this input stream is privacy sensitive and any
+ * concurrent capture is not permitted.
+ *
+ * This is off (PrivacySensitiveMode::Disabled) by default except when the input preset is
+ * InputPreset::VoiceRecognition or InputPreset::Camcorder
+ *
+ * Always takes precedence over default from input preset when set explicitly.
+ *
+ * Only relevant if the stream direction is Direction::Input and AAudio is used.
+ *
+ * Added in API level 30 to AAudio.
+ *
+ * @param privacySensitive PrivacySensitiveMode::Enabled if capture from this stream must be
+ * marked as privacy sensitive, PrivacySensitiveMode::Disabled if stream should be marked as
+ * not sensitive.
+ * @return pointer to the builder so calls can be chained
+ */
+ AudioStreamBuilder *setPrivacySensitiveMode(PrivacySensitiveMode privacySensitiveMode) {
+ mPrivacySensitiveMode = privacySensitiveMode;
+ return this;
+ }
+
+ /**
+ * Specifies whether the audio data of this output stream has already been processed for spatialization.
+ *
+ * If the stream has been processed for spatialization, setting this to true will prevent issues such as
+ * double-processing on platforms that will spatialize audio data.
+ *
+ * This is false by default.
+ *
+ * Available since API level 32.
+ *
+ * @param isContentSpatialized whether the content is already spatialized
+ * @return pointer to the builder so calls can be chained
+ */
+ AudioStreamBuilder *setIsContentSpatialized(bool isContentSpatialized) {
+ mIsContentSpatialized = isContentSpatialized;
+ return this;
+ }
+
+ /**
+ * Sets the behavior affecting whether spatialization will be used.
+ *
+ * The AAudio system will use this information to select whether the stream will go through a
+ * spatializer effect or not when the effect is supported and enabled.
+ *
+ * This is SpatializationBehavior::Never by default.
+ *
+ * Available since API level 32.
+ *
+ * @param spatializationBehavior the desired spatialization behavior
+ * @return pointer to the builder so calls can be chained
+ */
+ AudioStreamBuilder *setSpatializationBehavior(SpatializationBehavior spatializationBehavior) {
+ mSpatializationBehavior = spatializationBehavior;
+ return this;
+ }
+
+ /**
* Specifies an object to handle data related callbacks from the underlying API.
*
* <strong>Important: See AudioStreamCallback for restrictions on what may be called
@@ -570,6 +648,14 @@ public:
private:
/**
+ * Use this internally to implement opening with a shared_ptr.
+ *
+ * @param stream pointer to a variable to receive the stream address
+ * @return OBOE_OK if successful or a negative error code.
+ */
+ Result openStreamInternal(AudioStream **streamPP);
+
+ /**
* @param other
* @return true if channels, format and sample rate match
*/
diff --git a/include/oboe/AudioStreamCallback.h b/include/oboe/AudioStreamCallback.h
index 17d28ba7..8d8e2feb 100644
--- a/include/oboe/AudioStreamCallback.h
+++ b/include/oboe/AudioStreamCallback.h
@@ -48,7 +48,8 @@ public:
* write() on the stream that is making the callback.
*
* Note that numFrames can vary unless AudioStreamBuilder::setFramesPerCallback()
- * is called.
+ * is called. If AudioStreamBuilder::setFramesPerCallback() is NOT called then
+ * numFrames should always be <= AudioStream::getFramesPerBurst().
*
* Also note that this callback function should be considered a "real-time" function.
* It must not do anything that could cause an unbounded delay because that can cause the
diff --git a/include/oboe/Definitions.h b/include/oboe/Definitions.h
index be65c847..aaf4d640 100644
--- a/include/oboe/Definitions.h
+++ b/include/oboe/Definitions.h
@@ -98,6 +98,8 @@ namespace oboe {
/**
* Unspecified format. Format will be decided by Oboe.
+ * When calling getHardwareFormat(), this will be returned if
+ * the API is not supported.
*/
Unspecified = 0, // AAUDIO_FORMAT_UNSPECIFIED,
@@ -137,6 +139,19 @@ namespace oboe {
*/
I32 = 4, // AAUDIO_FORMAT_PCM_I32
+ /**
+ * This format is used for compressed audio wrapped in IEC61937 for HDMI
+ * or S/PDIF passthrough.
+ *
+ * Unlike PCM playback, the Android framework is not able to do format
+ * conversion for IEC61937. In that case, when IEC61937 is requested, sampling
+ * rate and channel count or channel mask must be specified. Otherwise, it may
+ * fail when opening the stream. Apps are able to get the correct configuration
+ * for the playback by calling AudioManager#getDevices(int).
+ *
+ * Available since API 34 (U).
+ */
+ IEC61937 = 5, // AAUDIO_FORMAT_IEC61937
};
/**
@@ -502,6 +517,11 @@ namespace oboe {
* The rest of the enums are channel position masks.
* Use the combinations of the channel position masks defined below instead of
* using those values directly.
+ *
+ * Channel masks are for input only, output only, or both input and output.
+ * These channel masks are different than those defined in AudioFormat.java.
+ * If an app gets a channel mask from Java API and wants to use it in Oboe,
+ * conversion should be done by the app.
*/
enum class ChannelMask : uint32_t { // aaudio_channel_mask_t
Unspecified = kUnspecified,
@@ -532,64 +552,108 @@ namespace oboe {
FrontWideLeft = 1 << 24,
FrontWideRight = 1 << 25,
+ /**
+ * Supported for Input and Output
+ */
Mono = FrontLeft,
+ /**
+ * Supported for Input and Output
+ */
Stereo = FrontLeft |
FrontRight,
+ /**
+ * Supported for only Output
+ */
CM2Point1 = FrontLeft |
FrontRight |
LowFrequency,
+ /**
+ * Supported for only Output
+ */
Tri = FrontLeft |
FrontRight |
FrontCenter,
+ /**
+ * Supported for only Output
+ */
TriBack = FrontLeft |
FrontRight |
BackCenter,
+ /**
+ * Supported for only Output
+ */
CM3Point1 = FrontLeft |
FrontRight |
FrontCenter |
LowFrequency,
+ /**
+ * Supported for Input and Output
+ */
CM2Point0Point2 = FrontLeft |
FrontRight |
TopSideLeft |
TopSideRight,
+ /**
+ * Supported for Input and Output
+ */
CM2Point1Point2 = CM2Point0Point2 |
LowFrequency,
+ /**
+ * Supported for Input and Output
+ */
CM3Point0Point2 = FrontLeft |
FrontRight |
FrontCenter |
TopSideLeft |
TopSideRight,
+ /**
+ * Supported for Input and Output
+ */
CM3Point1Point2 = CM3Point0Point2 |
LowFrequency,
+ /**
+ * Supported for only Output
+ */
Quad = FrontLeft |
FrontRight |
BackLeft |
BackRight,
+ /**
+ * Supported for only Output
+ */
QuadSide = FrontLeft |
FrontRight |
SideLeft |
SideRight,
+ /**
+ * Supported for only Output
+ */
Surround = FrontLeft |
FrontRight |
FrontCenter |
BackCenter,
+ /**
+ * Supported for only Output
+ */
Penta = Quad |
FrontCenter,
- // aka 5Point1Back
+ /**
+ * Supported for Input and Output. aka 5Point1Back
+ */
CM5Point1 = FrontLeft |
FrontRight |
FrontCenter |
@@ -597,6 +661,9 @@ namespace oboe {
BackLeft |
BackRight,
+ /**
+ * Supported for only Output
+ */
CM5Point1Side = FrontLeft |
FrontRight |
FrontCenter |
@@ -604,6 +671,9 @@ namespace oboe {
SideLeft |
SideRight,
+ /**
+ * Supported for only Output
+ */
CM6Point1 = FrontLeft |
FrontRight |
FrontCenter |
@@ -612,43 +682,159 @@ namespace oboe {
BackRight |
BackCenter,
+ /**
+ * Supported for only Output
+ */
CM7Point1 = CM5Point1 |
SideLeft |
SideRight,
+ /**
+ * Supported for only Output
+ */
CM5Point1Point2 = CM5Point1 |
TopSideLeft |
TopSideRight,
+ /**
+ * Supported for only Output
+ */
CM5Point1Point4 = CM5Point1 |
TopFrontLeft |
TopFrontRight |
TopBackLeft |
TopBackRight,
+ /**
+ * Supported for only Output
+ */
CM7Point1Point2 = CM7Point1 |
TopSideLeft |
TopSideRight,
+ /**
+ * Supported for only Output
+ */
CM7Point1Point4 = CM7Point1 |
TopFrontLeft |
TopFrontRight |
TopBackLeft |
TopBackRight,
+ /**
+ * Supported for only Output
+ */
CM9Point1Point4 = CM7Point1Point4 |
FrontWideLeft |
FrontWideRight,
+ /**
+ * Supported for only Output
+ */
CM9Point1Point6 = CM9Point1Point4 |
TopSideLeft |
TopSideRight,
+ /**
+ * Supported for only Input
+ */
FrontBack = FrontCenter |
BackCenter,
};
/**
+ * The spatialization behavior of the audio stream.
+ */
+ enum class SpatializationBehavior : int32_t {
+
+ /**
+ * Constant indicating that the spatialization behavior is not specified.
+ */
+ Unspecified = kUnspecified,
+
+ /**
+ * Constant indicating the audio content associated with these attributes will follow the
+ * default platform behavior with regards to which content will be spatialized or not.
+ */
+ Auto = 1,
+
+ /**
+ * Constant indicating the audio content associated with these attributes should never
+ * be spatialized.
+ */
+ Never = 2,
+ };
+
+ /**
+ * The PrivacySensitiveMode attribute determines whether an input stream can be shared
+ * with another privileged app, for example the Assistant.
+ *
+ * This allows to override the default behavior tied to the audio source (e.g
+ * InputPreset::VoiceCommunication is private by default but InputPreset::Unprocessed is not).
+ */
+ enum class PrivacySensitiveMode : int32_t {
+
+ /**
+ * When not explicitly requested, set privacy sensitive mode according to input preset:
+ * communication and camcorder captures are considered privacy sensitive by default.
+ */
+ Unspecified = kUnspecified,
+
+ /**
+ * Privacy sensitive mode disabled.
+ */
+ Disabled = 1,
+
+ /**
+ * Privacy sensitive mode enabled.
+ */
+ Enabled = 2,
+ };
+
+ /**
+ * Specifies whether audio may or may not be captured by other apps or the system for an
+ * output stream.
+ *
+ * Note that these match the equivalent values in AudioAttributes in the Android Java API.
+ *
+ * Added in API level 29 for AAudio.
+ */
+ enum class AllowedCapturePolicy : int32_t {
+ /**
+ * When not explicitly requested, set privacy sensitive mode according to the Usage.
+ * This should behave similarly to setting AllowedCapturePolicy::All.
+ */
+ Unspecified = kUnspecified,
+ /**
+ * Indicates that the audio may be captured by any app.
+ *
+ * For privacy, the following Usages can not be recorded: VoiceCommunication*,
+ * Notification*, Assistance* and Assistant.
+ *
+ * On Android Q, only Usage::Game and Usage::Media may be captured.
+ *
+ * See ALLOW_CAPTURE_BY_ALL in the AudioAttributes Java API.
+ */
+ All = 1,
+ /**
+ * Indicates that the audio may only be captured by system apps.
+ *
+ * System apps can capture for many purposes like accessibility, user guidance...
+ * but have strong restriction. See ALLOW_CAPTURE_BY_SYSTEM in the AudioAttributes Java API
+ * for what the system apps can do with the capture audio.
+ */
+ System = 2,
+ /**
+ * Indicates that the audio may not be recorded by any app, even if it is a system app.
+ *
+ * It is encouraged to use AllowedCapturePolicy::System instead of this value as system apps
+ * provide significant and useful features for the user (eg. accessibility).
+ * See ALLOW_CAPTURE_BY_NONE in the AudioAttributes Java API
+ */
+ None = 3,
+ };
+
+ /**
* On API 16 to 26 OpenSL ES will be used. When using OpenSL ES the optimal values for sampleRate and
* framesPerBurst are not known by the native code.
* On API 17+ these values should be obtained from the AudioManager using this code:
diff --git a/include/oboe/FullDuplexStream.h b/include/oboe/FullDuplexStream.h
new file mode 100644
index 00000000..d3ee3abf
--- /dev/null
+++ b/include/oboe/FullDuplexStream.h
@@ -0,0 +1,324 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef OBOE_FULL_DUPLEX_STREAM_
+#define OBOE_FULL_DUPLEX_STREAM_
+
+#include <cstdint>
+#include "oboe/Definitions.h"
+#include "oboe/AudioStream.h"
+#include "oboe/AudioStreamCallback.h"
+
+namespace oboe {
+
+/**
+ * FullDuplexStream can be used to synchronize an input and output stream.
+ *
+ * For the builder of the output stream, call setDataCallback() with this object.
+ *
+ * When both streams are ready, onAudioReady() of the output stream will call onBothStreamsReady().
+ * Callers must override onBothStreamsReady().
+ *
+ * To ensure best results, open an output stream before the input stream.
+ * Call inputBuilder.setBufferCapacityInFrames(mOutputStream->getBufferCapacityInFrames() * 2).
+ * Also, call inputBuilder.setSampleRate(mOutputStream->getSampleRate()).
+ *
+ * Callers must call setInputStream() and setOutputStream().
+ * Call start() to start both streams and stop() to stop both streams.
+ * Caller is responsible for closing both streams.
+ *
+ * Callers should handle error callbacks with setErrorCallback() for the output stream.
+ * When an error callback occurs for the output stream, Oboe will stop and close the output stream.
+ * The caller is responsible for stopping and closing the input stream.
+ * The caller should also reopen and restart both streams when the error callback is ErrorDisconnected.
+ * See the LiveEffect sample as an example of this.
+ *
+ */
+class FullDuplexStream : public AudioStreamDataCallback {
+public:
+ FullDuplexStream() {}
+ virtual ~FullDuplexStream() = default;
+
+ /**
+ * Sets the input stream. Calling this is mandatory.
+ *
+ * @param stream the output stream
+ */
+ void setInputStream(AudioStream *stream) {
+ mInputStream = stream;
+ }
+
+ /**
+ * Gets the input stream
+ *
+ * @return the input stream
+ */
+ AudioStream *getInputStream() {
+ return mInputStream;
+ }
+
+ /**
+ * Sets the output stream. Calling this is mandatory.
+ *
+ * @param stream the output stream
+ */
+ void setOutputStream(AudioStream *stream) {
+ mOutputStream = stream;
+ }
+
+ /**
+ * Gets the output stream
+ *
+ * @return the output stream
+ */
+ AudioStream *getOutputStream() {
+ return mOutputStream;
+ }
+
+ /**
+ * Attempts to start both streams. Please call setInputStream() and setOutputStream() before
+ * calling this function.
+ *
+ * @return result of the operation
+ */
+ virtual Result start() {
+ mCountCallbacksToDrain = kNumCallbacksToDrain;
+ mCountInputBurstsCushion = mNumInputBurstsCushion;
+ mCountCallbacksToDiscard = kNumCallbacksToDiscard;
+
+ // Determine maximum size that could possibly be called.
+ int32_t bufferSize = getOutputStream()->getBufferCapacityInFrames()
+ * getOutputStream()->getChannelCount();
+ if (bufferSize > mBufferSize) {
+ mInputBuffer = std::make_unique<float[]>(bufferSize);
+ mBufferSize = bufferSize;
+ }
+
+ oboe::Result result = getInputStream()->requestStart();
+ if (result != oboe::Result::OK) {
+ return result;
+ }
+ return getOutputStream()->requestStart();
+ }
+
+ /**
+ * Stops both streams. Returns Result::OK if neither stream had an error during close.
+ *
+ * @return result of the operation
+ */
+ virtual Result stop() {
+ Result outputResult = Result::OK;
+ Result inputResult = Result::OK;
+ if (getOutputStream()) {
+ outputResult = mOutputStream->requestStop();
+ }
+ if (getInputStream()) {
+ inputResult = mInputStream->requestStop();
+ }
+ if (outputResult != Result::OK) {
+ return outputResult;
+ } else {
+ return inputResult;
+ }
+ }
+
+ /**
+ * Reads input from the input stream. Callers should not call this directly as this is called
+ * in onAudioReady().
+ *
+ * @param numFrames
+ * @return result of the operation
+ */
+ virtual ResultWithValue<int32_t> readInput(int32_t numFrames) {
+ return getInputStream()->read(mInputBuffer.get(), numFrames, 0 /* timeout */);
+ }
+
+ /**
+ * Called when data is available on both streams.
+ * Caller should override this method.
+ * numInputFrames and numOutputFrames may be zero.
+ *
+ * @param inputData buffer containing input data
+ * @param numInputFrames number of input frames
+ * @param outputData a place to put output data
+ * @param numOutputFrames number of output frames
+ * @return DataCallbackResult::Continue or DataCallbackResult::Stop
+ */
+ virtual DataCallbackResult onBothStreamsReady(
+ const void *inputData,
+ int numInputFrames,
+ void *outputData,
+ int numOutputFrames
+ ) = 0;
+
+ /**
+ * Called when the output stream is ready to process audio.
+ * This in return calls onBothStreamsReady() when data is available on both streams.
+ * Callers should call this function when the output stream is ready.
+ * Callers must override onBothStreamsReady().
+ *
+ * @param audioStream pointer to the associated stream
+ * @param audioData a place to put output data
+ * @param numFrames number of frames to be processed
+ * @return DataCallbackResult::Continue or DataCallbackResult::Stop
+ *
+ */
+ DataCallbackResult onAudioReady(
+ AudioStream * /*audioStream*/,
+ void *audioData,
+ int numFrames) {
+ DataCallbackResult callbackResult = DataCallbackResult::Continue;
+ int32_t actualFramesRead = 0;
+
+ // Silence the output.
+ int32_t numBytes = numFrames * getOutputStream()->getBytesPerFrame();
+ memset(audioData, 0 /* value */, numBytes);
+
+ if (mCountCallbacksToDrain > 0) {
+ // Drain the input.
+ int32_t totalFramesRead = 0;
+ do {
+ ResultWithValue<int32_t> result = readInput(numFrames);
+ if (!result) {
+ // Ignore errors because input stream may not be started yet.
+ break;
+ }
+ actualFramesRead = result.value();
+ totalFramesRead += actualFramesRead;
+ } while (actualFramesRead > 0);
+ // Only counts if we actually got some data.
+ if (totalFramesRead > 0) {
+ mCountCallbacksToDrain--;
+ }
+
+ } else if (mCountInputBurstsCushion > 0) {
+ // Let the input fill up a bit so we are not so close to the write pointer.
+ mCountInputBurstsCushion--;
+
+ } else if (mCountCallbacksToDiscard > 0) {
+ mCountCallbacksToDiscard--;
+ // Ignore. Allow the input to reach to equilibrium with the output.
+ ResultWithValue<int32_t> resultAvailable = getInputStream()->getAvailableFrames();
+ if (!resultAvailable) {
+ callbackResult = DataCallbackResult::Stop;
+ } else {
+ int32_t framesAvailable = resultAvailable.value();
+ if (framesAvailable >= mMinimumFramesBeforeRead) {
+ ResultWithValue<int32_t> resultRead = readInput(numFrames);
+ if (!resultRead) {
+ callbackResult = DataCallbackResult::Stop;
+ }
+ }
+ }
+ } else {
+ int32_t framesRead = 0;
+ ResultWithValue<int32_t> resultAvailable = getInputStream()->getAvailableFrames();
+ if (!resultAvailable) {
+ callbackResult = DataCallbackResult::Stop;
+ } else {
+ int32_t framesAvailable = resultAvailable.value();
+ if (framesAvailable >= mMinimumFramesBeforeRead) {
+ // Read data into input buffer.
+ ResultWithValue<int32_t> resultRead = readInput(numFrames);
+ if (!resultRead) {
+ callbackResult = DataCallbackResult::Stop;
+ } else {
+ framesRead = resultRead.value();
+ }
+ }
+ }
+
+ if (callbackResult == DataCallbackResult::Continue) {
+ callbackResult = onBothStreamsReady(mInputBuffer.get(), framesRead,
+ audioData, numFrames);
+ }
+ }
+
+ if (callbackResult == DataCallbackResult::Stop) {
+ getInputStream()->requestStop();
+ }
+
+ return callbackResult;
+ }
+
+ /**
+ *
+ * This is a cushion between the DSP and the application processor cursors to prevent collisions.
+ * Typically 0 for latency measurements or 1 for glitch tests.
+ *
+ * @param numBursts number of bursts to leave in the input buffer as a cushion
+ */
+ void setNumInputBurstsCushion(int32_t numBursts) {
+ mNumInputBurstsCushion = numBursts;
+ }
+
+ /**
+ * Get the number of bursts left in the input buffer as a cushion.
+ *
+ * @return number of bursts in the input buffer as a cushion
+ */
+ int32_t getNumInputBurstsCushion() const {
+ return mNumInputBurstsCushion;
+ }
+
+ /**
+ * Minimum number of frames in the input stream buffer before calling readInput().
+ *
+ * @param numFrames number of bursts in the input buffer as a cushion
+ */
+ void setMinimumFramesBeforeRead(int32_t numFrames) {
+ mMinimumFramesBeforeRead = numFrames;
+ }
+
+ /**
+ * Gets the minimum number of frames in the input stream buffer before calling readInput().
+ *
+ * @return minimum number of frames before reading
+ */
+ int32_t getMinimumFramesBeforeRead() const {
+ return mMinimumFramesBeforeRead;
+ }
+
+private:
+
+ // TODO add getters and setters
+ static constexpr int32_t kNumCallbacksToDrain = 20;
+ static constexpr int32_t kNumCallbacksToDiscard = 30;
+
+ // let input fill back up, usually 0 or 1
+ int32_t mNumInputBurstsCushion = 0;
+ int32_t mMinimumFramesBeforeRead = 0;
+
+ // We want to reach a state where the input buffer is empty and
+ // the output buffer is full.
+ // These are used in order.
+ // Drain several callback so that input is empty.
+ int32_t mCountCallbacksToDrain = kNumCallbacksToDrain;
+ // Let the input fill back up slightly so we don't run dry.
+ int32_t mCountInputBurstsCushion = mNumInputBurstsCushion;
+ // Discard some callbacks so the input and output reach equilibrium.
+ int32_t mCountCallbacksToDiscard = kNumCallbacksToDiscard;
+
+ AudioStream *mInputStream = nullptr;
+ AudioStream *mOutputStream = nullptr;
+
+ int32_t mBufferSize = 0;
+ std::unique_ptr<float[]> mInputBuffer;
+};
+
+} // namespace oboe
+
+#endif //OBOE_FULL_DUPLEX_STREAM_
diff --git a/include/oboe/Oboe.h b/include/oboe/Oboe.h
index ea595af6..b9c948af 100644
--- a/include/oboe/Oboe.h
+++ b/include/oboe/Oboe.h
@@ -35,5 +35,6 @@
#include "oboe/StabilizedCallback.h"
#include "oboe/FifoBuffer.h"
#include "oboe/OboeExtensions.h"
+#include "oboe/FullDuplexStream.h"
#endif //OBOE_OBOE_H
diff --git a/include/oboe/Utilities.h b/include/oboe/Utilities.h
index a5e91709..f0f41865 100644
--- a/include/oboe/Utilities.h
+++ b/include/oboe/Utilities.h
@@ -82,6 +82,16 @@ int getPropertyInteger(const char * name, int defaultValue);
*/
int getSdkVersion();
+/**
+ * Returns whether a device is on a pre-release SDK that is at least the specified codename
+ * version.
+ *
+ * @param codename the code name to verify.
+ * @return boolean of whether the device is on a pre-release SDK and is at least the specified
+ * codename
+ */
+bool isAtLeastPreReleaseCodename(const std::string& codename);
+
int getChannelCountFromChannelMask(ChannelMask channelMask);
} // namespace oboe
diff --git a/include/oboe/Version.h b/include/oboe/Version.h
index 6bcc045d..a410f0f4 100644
--- a/include/oboe/Version.h
+++ b/include/oboe/Version.h
@@ -34,10 +34,10 @@
#define OBOE_VERSION_MAJOR 1
// Type: 8-bit unsigned int. Min value: 0 Max value: 255. See below for description.
-#define OBOE_VERSION_MINOR 7
+#define OBOE_VERSION_MINOR 8
// Type: 16-bit unsigned int. Min value: 0 Max value: 65535. See below for description.
-#define OBOE_VERSION_PATCH 1
+#define OBOE_VERSION_PATCH 2
#define OBOE_STRINGIFY(x) #x
#define OBOE_TOSTRING(x) OBOE_STRINGIFY(x)
diff --git a/samples/LiveEffect/build.gradle b/samples/LiveEffect/build.gradle
index f336d2e2..50fb0953 100644
--- a/samples/LiveEffect/build.gradle
+++ b/samples/LiveEffect/build.gradle
@@ -1,12 +1,12 @@
apply plugin: 'com.android.application'
android {
- compileSdkVersion 33
+ compileSdkVersion 34
defaultConfig {
applicationId 'com.google.oboe.samples.liveeffect'
minSdkVersion 21
- targetSdkVersion 33
+ targetSdkVersion 34
versionCode 1
versionName '1.0'
ndk {
diff --git a/samples/LiveEffect/src/main/cpp/CMakeLists.txt b/samples/LiveEffect/src/main/cpp/CMakeLists.txt
index fbf9becf..4f16621c 100644
--- a/samples/LiveEffect/src/main/cpp/CMakeLists.txt
+++ b/samples/LiveEffect/src/main/cpp/CMakeLists.txt
@@ -26,7 +26,6 @@ add_subdirectory(${OBOE_DIR} ./oboe-bin)
add_library(liveEffect
SHARED
LiveEffectEngine.cpp
- FullDuplexStream.cpp
jni_bridge.cpp
${SAMPLE_ROOT_DIR}/debug-utils/trace.cpp)
target_include_directories(liveEffect
diff --git a/samples/LiveEffect/src/main/cpp/FullDuplexPass.h b/samples/LiveEffect/src/main/cpp/FullDuplexPass.h
index 6f5d05ff..4c8bbd6d 100644
--- a/samples/LiveEffect/src/main/cpp/FullDuplexPass.h
+++ b/samples/LiveEffect/src/main/cpp/FullDuplexPass.h
@@ -17,16 +17,12 @@
#ifndef SAMPLES_FULLDUPLEXPASS_H
#define SAMPLES_FULLDUPLEXPASS_H
-#include "FullDuplexStream.h"
-
-class FullDuplexPass : public FullDuplexStream {
+class FullDuplexPass : public oboe::FullDuplexStream {
public:
virtual oboe::DataCallbackResult
onBothStreamsReady(
- std::shared_ptr<oboe::AudioStream> inputStream,
const void *inputData,
int numInputFrames,
- std::shared_ptr<oboe::AudioStream> outputStream,
void *outputData,
int numOutputFrames) {
// Copy the input samples to the output with a little arbitrary gain change.
@@ -36,7 +32,7 @@ public:
float *outputFloats = static_cast<float *>(outputData);
// It also assumes the channel count for each stream is the same.
- int32_t samplesPerFrame = outputStream->getChannelCount();
+ int32_t samplesPerFrame = getOutputStream()->getChannelCount();
int32_t numInputSamples = numInputFrames * samplesPerFrame;
int32_t numOutputSamples = numOutputFrames * samplesPerFrame;
diff --git a/samples/LiveEffect/src/main/cpp/FullDuplexStream.cpp b/samples/LiveEffect/src/main/cpp/FullDuplexStream.cpp
deleted file mode 100644
index e38bb5f7..00000000
--- a/samples/LiveEffect/src/main/cpp/FullDuplexStream.cpp
+++ /dev/null
@@ -1,128 +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 "FullDuplexStream.h"
-
-oboe::DataCallbackResult FullDuplexStream::onAudioReady(
- oboe::AudioStream *outputStream,
- void *audioData,
- int numFrames) {
- oboe::DataCallbackResult callbackResult = oboe::DataCallbackResult::Continue;
- int32_t actualFramesRead = 0;
-
- // Silence the output.
- int32_t numBytes = numFrames * outputStream->getBytesPerFrame();
- memset(audioData, 0 /* value */, numBytes);
-
- if (mCountCallbacksToDrain > 0) {
- // Drain the input.
- int32_t totalFramesRead = 0;
- do {
- oboe::ResultWithValue<int32_t> result = mInputStream->read(mInputBuffer.get(),
- numFrames,
- 0 /* timeout */);
- if (!result) {
- // Ignore errors because input stream may not be started yet.
- break;
- }
- actualFramesRead = result.value();
- totalFramesRead += actualFramesRead;
- } while (actualFramesRead > 0);
- // Only counts if we actually got some data.
- if (totalFramesRead > 0) {
- mCountCallbacksToDrain--;
- }
-
- } else if (mCountInputBurstsCushion > 0) {
- // Let the input fill up a bit so we are not so close to the write pointer.
- mCountInputBurstsCushion--;
-
- } else if (mCountCallbacksToDiscard > 0) {
- // Ignore. Allow the input to reach to equilibrium with the output.
- oboe::ResultWithValue<int32_t> result = mInputStream->read(mInputBuffer.get(),
- numFrames,
- 0 /* timeout */);
- if (!result) {
- callbackResult = oboe::DataCallbackResult::Stop;
- }
- mCountCallbacksToDiscard--;
-
- } else {
- // Read data into input buffer.
- oboe::ResultWithValue<int32_t> result = mInputStream->read(mInputBuffer.get(),
- numFrames,
- 0 /* timeout */);
- if (!result) {
- callbackResult = oboe::DataCallbackResult::Stop;
- } else {
- int32_t framesRead = result.value();
-
- callbackResult = onBothStreamsReady(
- mInputStream, mInputBuffer.get(), framesRead,
- mOutputStream, audioData, numFrames
- );
- }
- }
-
- if (callbackResult == oboe::DataCallbackResult::Stop) {
- mInputStream->requestStop();
- }
-
- return callbackResult;
-}
-
-oboe::Result FullDuplexStream::start() {
- mCountCallbacksToDrain = kNumCallbacksToDrain;
- mCountInputBurstsCushion = mNumInputBurstsCushion;
- mCountCallbacksToDiscard = kNumCallbacksToDiscard;
-
- // Determine maximum size that could possibly be called.
- int32_t bufferSize = mOutputStream->getBufferCapacityInFrames()
- * mOutputStream->getChannelCount();
- if (bufferSize > mBufferSize) {
- mInputBuffer = std::make_unique<float[]>(bufferSize);
- mBufferSize = bufferSize;
- }
- oboe::Result result = mInputStream->requestStart();
- if (result != oboe::Result::OK) {
- return result;
- }
- return mOutputStream->requestStart();
-}
-
-oboe::Result FullDuplexStream::stop() {
- oboe::Result outputResult = oboe::Result::OK;
- oboe::Result inputResult = oboe::Result::OK;
- if (mOutputStream) {
- outputResult = mOutputStream->requestStop();
- }
- if (mInputStream) {
- inputResult = mInputStream->requestStop();
- }
- if (outputResult != oboe::Result::OK) {
- return outputResult;
- } else {
- return inputResult;
- }
-}
-
-int32_t FullDuplexStream::getNumInputBurstsCushion() const {
- return mNumInputBurstsCushion;
-}
-
-void FullDuplexStream::setNumInputBurstsCushion(int32_t numBursts) {
- FullDuplexStream::mNumInputBurstsCushion = numBursts;
-}
diff --git a/samples/LiveEffect/src/main/cpp/FullDuplexStream.h b/samples/LiveEffect/src/main/cpp/FullDuplexStream.h
deleted file mode 100644
index c30bc04a..00000000
--- a/samples/LiveEffect/src/main/cpp/FullDuplexStream.h
+++ /dev/null
@@ -1,102 +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 OBOE_FULL_DUPLEX_STREAM_H
-#define OBOE_FULL_DUPLEX_STREAM_H
-
-#include <unistd.h>
-#include <sys/types.h>
-
-#include "oboe/Oboe.h"
-
-class FullDuplexStream : public oboe::AudioStreamCallback {
-public:
- FullDuplexStream() {}
- virtual ~FullDuplexStream() = default;
-
- void setInputStream(std::shared_ptr<oboe::AudioStream> stream) {
- mInputStream = stream;
- }
-
- void setOutputStream(std::shared_ptr<oboe::AudioStream> stream) {
- mOutputStream = stream;
- }
-
- virtual oboe::Result start();
-
- virtual oboe::Result stop();
-
- /**
- * Called when data is available on both streams.
- * App should override this method.
- */
- virtual oboe::DataCallbackResult onBothStreamsReady(
- std::shared_ptr<oboe::AudioStream> inputStream,
- const void *inputData,
- int numInputFrames,
- std::shared_ptr<oboe::AudioStream> outputStream,
- void *outputData,
- int numOutputFrames
- ) = 0;
-
- /**
- * Called by Oboe when the stream is ready to process audio.
- * This implements the stream synchronization. App should NOT override this method.
- */
- oboe::DataCallbackResult onAudioReady(
- oboe::AudioStream *audioStream,
- void *audioData,
- int numFrames) override;
-
- int32_t getNumInputBurstsCushion() const;
-
- /**
- * Number of bursts to leave in the input buffer as a cushion.
- * Typically 0 for latency measurements
- * or 1 for glitch tests.
- *
- * @param mNumInputBurstsCushion
- */
- void setNumInputBurstsCushion(int32_t numInputBurstsCushion);
-
-private:
-
- // TODO add getters and setters
- static constexpr int32_t kNumCallbacksToDrain = 20;
- static constexpr int32_t kNumCallbacksToDiscard = 30;
-
- // let input fill back up, usually 0 or 1
- int32_t mNumInputBurstsCushion = 1;
-
- // We want to reach a state where the input buffer is empty and
- // the output buffer is full.
- // These are used in order.
- // Drain several callback so that input is empty.
- int32_t mCountCallbacksToDrain = kNumCallbacksToDrain;
- // Let the input fill back up slightly so we don't run dry.
- int32_t mCountInputBurstsCushion = mNumInputBurstsCushion;
- // Discard some callbacks so the input and output reach equilibrium.
- int32_t mCountCallbacksToDiscard = kNumCallbacksToDiscard;
-
- std::shared_ptr<oboe::AudioStream> mInputStream;
- std::shared_ptr<oboe::AudioStream> mOutputStream;
-
- int32_t mBufferSize = 0;
- std::unique_ptr<float[]> mInputBuffer;
-};
-
-
-#endif //OBOE_FULL_DUPLEX_STREAM_H
diff --git a/samples/LiveEffect/src/main/cpp/LiveEffectEngine.cpp b/samples/LiveEffect/src/main/cpp/LiveEffectEngine.cpp
index e02e0090..140dd328 100644
--- a/samples/LiveEffect/src/main/cpp/LiveEffectEngine.cpp
+++ b/samples/LiveEffect/src/main/cpp/LiveEffectEngine.cpp
@@ -102,8 +102,8 @@ oboe::Result LiveEffectEngine::openStreams() {
}
warnIfNotLowLatency(mRecordingStream);
- mFullDuplexPass.setInputStream(mRecordingStream);
- mFullDuplexPass.setOutputStream(mPlayStream);
+ mFullDuplexPass.setInputStream(mRecordingStream.get());
+ mFullDuplexPass.setOutputStream(mPlayStream.get());
return result;
}
@@ -235,4 +235,20 @@ void LiveEffectEngine::onErrorAfterClose(oboe::AudioStream *oboeStream,
LOGE("%s stream Error after close: %s",
oboe::convertToText(oboeStream->getDirection()),
oboe::convertToText(error));
+
+ // Stop the Full Duplex stream.
+ // Since the error callback occurs only for the output stream, close the input stream.
+ mFullDuplexPass.stop();
+ mFullDuplexPass.setOutputStream(nullptr);
+ closeStream(mRecordingStream);
+ mFullDuplexPass.setInputStream(nullptr);
+
+ // Restart the stream if the error is a disconnect.
+ if (error == oboe::Result::ErrorDisconnected) {
+ LOGI("Restarting AudioStream");
+ oboe::Result result = openStreams();
+ if (result == oboe::Result::OK) {
+ mFullDuplexPass.start();
+ }
+ }
}
diff --git a/samples/MegaDrone/README.md b/samples/MegaDrone/README.md
index a3b40347..cf312598 100644
--- a/samples/MegaDrone/README.md
+++ b/samples/MegaDrone/README.md
@@ -12,8 +12,6 @@ This sample demonstrates how to obtain the lowest latency and optimal computatio
4) Setting the buffer size to 2 bursts
5) Using the `-Ofast` compiler optimization flag, even when building the `Debug` variant
6) Using [`getExclusiveCores`](https://developer.android.com/reference/android/os/Process#getExclusiveCores()) (API 24+) and thread affinity to bind the audio thread to the best available CPU core(s)
-7) Using a `StabilizedCallback` which aims to spend a fixed percentage of the callback time to avoid CPU frequency scaling ([video explanation](https://www.youtube.com/watch?v=C0BPXZIvG-Q&feature=youtu.be&t=1158))
-
This code was presented at [AES Milan](http://www.aes.org/events/144/) and [Droidcon Berlin](https://www.de.droidcon.com/) as part of a talk on Oboe.
diff --git a/samples/MegaDrone/build.gradle b/samples/MegaDrone/build.gradle
index 93c372c0..1096b111 100644
--- a/samples/MegaDrone/build.gradle
+++ b/samples/MegaDrone/build.gradle
@@ -1,11 +1,11 @@
apply plugin: 'com.android.application'
android {
- compileSdkVersion 33
+ compileSdkVersion 34
defaultConfig {
applicationId "com.google.oboe.samples.megadrone"
minSdkVersion 21
- targetSdkVersion 33
+ targetSdkVersion 34
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
diff --git a/samples/MegaDrone/src/main/java/com/google/oboe/samples/megadrone/MainActivity.java b/samples/MegaDrone/src/main/java/com/google/oboe/samples/megadrone/MainActivity.java
index a3c7a94f..5c74724f 100644
--- a/samples/MegaDrone/src/main/java/com/google/oboe/samples/megadrone/MainActivity.java
+++ b/samples/MegaDrone/src/main/java/com/google/oboe/samples/megadrone/MainActivity.java
@@ -50,8 +50,8 @@ public class MainActivity extends AppCompatActivity {
@Override
protected void onResume(){
- mEngineHandle = startEngine(getExclusiveCores());
super.onResume();
+ mEngineHandle = startEngine(getExclusiveCores());
}
@Override
diff --git a/samples/README.md b/samples/README.md
index bfee3de5..843c8113 100644
--- a/samples/README.md
+++ b/samples/README.md
@@ -8,9 +8,10 @@ sine wave when you tap the screen. (Java)
1. [RhythmGame](RhythmGame): A simple rhythm game where you copy the clap patterns you hear by tapping on the screen.
There is an associated codelab to follow along with. (Java)
1. [MegaDrone](MegaDrone): A one hundred oscillator synthesizer, demonstrates low latency and CPU performance. (Java)
-1. [DrumThumper](drumthumper): A drum pad that plays sounds from loaded WAV files. (Java)
+1. [DrumThumper](drumthumper): A drum pad that plays sounds from loaded WAV files. (Kotlin)
1. [LiveEffect](LiveEffect): Loops audio from input stream to output stream to demonstrate duplex capability. (Java)
-1. [SoundBoard](SoundBoard): A 30 note dynamic synthesizer, demonstating combining signals. (Kotlin)
+1. [SoundBoard](SoundBoard): A 25 to 40 note dynamic synthesizer, demonstating combining signals. The stream restarts
+when the display rotates. (Kotlin)
Pre-requisites
-------------
diff --git a/samples/RhythmGame/build.gradle b/samples/RhythmGame/build.gradle
index 88681f43..c453fcb5 100644
--- a/samples/RhythmGame/build.gradle
+++ b/samples/RhythmGame/build.gradle
@@ -1,10 +1,10 @@
apply plugin: 'com.android.application'
android {
- compileSdkVersion 33
+ compileSdkVersion 34
defaultConfig {
applicationId "com.google.oboe.samples.rhythmgame"
- targetSdkVersion 33
+ targetSdkVersion 34
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
diff --git a/samples/RhythmGame/src/main/java/com/google/oboe/samples/rhythmgame/MainActivity.java b/samples/RhythmGame/src/main/java/com/google/oboe/samples/rhythmgame/MainActivity.java
index 8c69c16b..d370641c 100644
--- a/samples/RhythmGame/src/main/java/com/google/oboe/samples/rhythmgame/MainActivity.java
+++ b/samples/RhythmGame/src/main/java/com/google/oboe/samples/rhythmgame/MainActivity.java
@@ -52,8 +52,8 @@ public class MainActivity extends AppCompatActivity {
}
protected void onPause(){
- super.onPause();
native_onStop();
+ super.onPause();
}
static void setDefaultStreamValues(Context context) {
diff --git a/samples/RhythmGame/test/run_tests.sh b/samples/RhythmGame/test/run_tests.sh
index cb888d95..91060a0a 100755
--- a/samples/RhythmGame/test/run_tests.sh
+++ b/samples/RhythmGame/test/run_tests.sh
@@ -74,7 +74,7 @@ fi
# Configure the build
echo "Building tests for ${ABI} using ${PLATFORM}"
-CMAKE_ARGS="-H. \
+CMAKE_ARGS="-S. \
-B${BUILD_DIR} \
-DANDROID_ABI=${ABI} \
-DANDROID_PLATFORM=${PLATFORM} \
diff --git a/samples/SoundBoard/README.md b/samples/SoundBoard/README.md
index 9c4090b5..23e14e68 100644
--- a/samples/SoundBoard/README.md
+++ b/samples/SoundBoard/README.md
@@ -9,7 +9,6 @@ This sample demonstrates how to obtain the lowest latency and optimal computatio
3) Setting sharing mode to Exclusive
4) Setting the buffer size to 2 bursts
5) Using the `-Ofast` compiler optimization flag, even when building the `Debug` variant
-7) Using a `StabilizedCallback` which aims to spend a fixed percentage of the callback time to avoid CPU frequency scaling ([video explanation](https://www.youtube.com/watch?v=C0BPXZIvG-Q&feature=youtu.be&t=1158))
The [following article explaining how to debug CPU performance problems](https://medium.com/@donturner/debugging-audio-glitches-on-android-ed10782f9c64) may also be useful when looking at this code.
diff --git a/samples/SoundBoard/build.gradle b/samples/SoundBoard/build.gradle
index 81cb6050..fdfe8a72 100644
--- a/samples/SoundBoard/build.gradle
+++ b/samples/SoundBoard/build.gradle
@@ -1,11 +1,14 @@
-apply plugin: 'com.android.application'
+plugins {
+ id 'com.android.application'
+ id 'org.jetbrains.kotlin.android'
+}
android {
- compileSdkVersion 33
+ compileSdkVersion 34
defaultConfig {
applicationId "com.google.oboe.samples.soundboard"
minSdkVersion 21
- targetSdkVersion 33
+ targetSdkVersion 34
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
diff --git a/samples/SoundBoard/src/main/AndroidManifest.xml b/samples/SoundBoard/src/main/AndroidManifest.xml
index ed620392..707b32a3 100644
--- a/samples/SoundBoard/src/main/AndroidManifest.xml
+++ b/samples/SoundBoard/src/main/AndroidManifest.xml
@@ -10,7 +10,6 @@
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name="com.google.oboe.samples.soundboard.MainActivity"
- android:screenOrientation="portrait"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
diff --git a/samples/SoundBoard/src/main/cpp/Synth.h b/samples/SoundBoard/src/main/cpp/Synth.h
index 47aa2fa7..ef28a3a6 100644
--- a/samples/SoundBoard/src/main/cpp/Synth.h
+++ b/samples/SoundBoard/src/main/cpp/Synth.h
@@ -36,6 +36,8 @@ public:
}
Synth(const int32_t sampleRate, const int32_t channelCount, const int32_t numSignals) {
+ mNumSignals = numSignals;
+ mOscs = std::make_unique<SynthSound[]>(numSignals);
float curFrequency = kOscBaseFrequency;
float curAmplitude = kOscBaseAmplitude;
for (int i = 0; i < numSignals; ++i) {
@@ -82,7 +84,7 @@ public:
private:
// Rendering objects
int32_t mNumSignals;
- std::array<SynthSound, kMaxTracks> mOscs;
+ std::unique_ptr<SynthSound[]> mOscs;
Mixer mMixer;
MonoToStereo mConverter = MonoToStereo(&mMixer);
IRenderableAudio *mOutputStage; // This will point to either the mixer or converter, so it needs to be raw
diff --git a/samples/SoundBoard/src/main/java/com/google/oboe/samples/soundboard/MainActivity.java b/samples/SoundBoard/src/main/java/com/google/oboe/samples/soundboard/MainActivity.java
deleted file mode 100644
index bceb0079..00000000
--- a/samples/SoundBoard/src/main/java/com/google/oboe/samples/soundboard/MainActivity.java
+++ /dev/null
@@ -1,111 +0,0 @@
-package com.google.oboe.samples.soundboard;
-
-/*
- * Copyright 2021 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.
- */
-
-import androidx.appcompat.app.AppCompatActivity;
-
-import android.app.Activity;
-import android.content.Context;
-import android.graphics.Rect;
-import android.media.AudioManager;
-import android.os.Build;
-import android.os.Bundle;
-import android.util.DisplayMetrics;
-
-import java.util.ArrayList;
-
-import static java.lang.Math.min;
-
-public class MainActivity extends AppCompatActivity {
-
- private final String TAG = MainActivity.class.toString();
- private final int NUM_ROWS = 6;
- private final int NUM_COLUMNS = 5;
- private static long mEngineHandle = 0;
-
- private native long startEngine(int numSignals);
- private native void stopEngine(long engineHandle);
-
- private static native void native_setDefaultStreamValues(int sampleRate, int framesPerBurst);
-
- // Used to load the 'native-lib' library on application startup.
- static {
- System.loadLibrary("soundboard");
- }
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- }
-
- @Override
- protected void onResume(){
- setDefaultStreamValues(this);
- mEngineHandle = startEngine(NUM_ROWS * NUM_COLUMNS);
- createMusicTiles(this);
- super.onResume();
- }
-
- @Override
- protected void onPause(){
- stopEngine(mEngineHandle);
- super.onPause();
- }
-
- static void setDefaultStreamValues(Context context) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1){
- AudioManager myAudioMgr = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
- String sampleRateStr = myAudioMgr.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE);
- int defaultSampleRate = Integer.parseInt(sampleRateStr);
- String framesPerBurstStr = myAudioMgr.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER);
- int defaultFramesPerBurst = Integer.parseInt(framesPerBurstStr);
-
- native_setDefaultStreamValues(defaultSampleRate, defaultFramesPerBurst);
- }
- }
-
- void createMusicTiles(Context context) {
- DisplayMetrics displayMetrics = new DisplayMetrics();
- ((Activity)context).getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
- int height = displayMetrics.heightPixels;
- int width = displayMetrics.widthPixels;
-
- // 5 by 6 tiles
- final int numRows = NUM_ROWS;
- final int numColumns = NUM_COLUMNS;
- final int tileLength = min(height / (numRows), width / (numColumns));
- final int xStartLocation = (width - tileLength * numColumns) / 2;
- // Height isn't a perfect measurement so shift the location slightly up from the "center"
- final int yStartLocation = (height - tileLength * numRows) / 2 / 2;
-
- ArrayList<Rect> rectangles = new ArrayList<Rect>();
- for (int i = 0; i < numRows; i++) {
- for (int j = 0; j < numColumns; j++) {
- Rect rectangle = new Rect(xStartLocation + j * tileLength,
- yStartLocation + i * tileLength,
- xStartLocation + j * tileLength + tileLength,
- yStartLocation + i * tileLength + tileLength);
- rectangles.add(rectangle);
- }
- }
-
- setContentView(new MusicTileView(this, rectangles, new NoteListener(mEngineHandle)));
- }
-}
-
-
diff --git a/samples/SoundBoard/src/main/java/com/google/oboe/samples/soundboard/MusicTileView.java b/samples/SoundBoard/src/main/java/com/google/oboe/samples/soundboard/MusicTileView.java
deleted file mode 100644
index 5355067c..00000000
--- a/samples/SoundBoard/src/main/java/com/google/oboe/samples/soundboard/MusicTileView.java
+++ /dev/null
@@ -1,169 +0,0 @@
-package com.google.oboe.samples.soundboard;
-
-/*
- * Copyright 2021 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.
- */
-
-import android.content.Context;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Paint;
-import android.graphics.PointF;
-import android.graphics.Rect;
-import android.util.SparseArray;
-import android.view.MotionEvent;
-import android.view.View;
-
-import java.util.ArrayList;
-
-public class MusicTileView extends View {
- private ArrayList<Rect> mRectangles;
- private boolean[] mIsPressedPerRectangle;
- private Paint mPaint;
- private SparseArray<PointF> mLocationsOfFingers;
- private TileListener mTileListener;
-
- public interface TileListener {
- public void onTileOn(int index);
- public void onTileOff(int index);
- }
-
- public MusicTileView(Context context, ArrayList<Rect> rectangles, TileListener tileListener) {
- super(context);
-
- mRectangles = rectangles;
- mIsPressedPerRectangle = new boolean[rectangles.size()];
- mPaint = new Paint();
- mLocationsOfFingers = new SparseArray<PointF>();
-
- mTileListener = tileListener;
- }
-
- private int getIndexFromLocation(PointF pointF) {
- for (int i = 0; i < mRectangles.size(); i++) {
- if (pointF.x > mRectangles.get(i).left &&
- pointF.x < mRectangles.get(i).right &&
- pointF.y > mRectangles.get(i).top &&
- pointF.y < mRectangles.get(i).bottom) {
- return i;
- }
- }
- return -1;
- }
-
- @Override
- protected void onDraw(Canvas canvas) {
- for (int i = 0; i < mRectangles.size(); i++) {
- mPaint.setStyle(Paint.Style.FILL);
- if (mIsPressedPerRectangle[i]) {
- mPaint.setColor(Color.rgb(128, 0, 0));
- } else {
- mPaint.setColor(Color.BLACK);
- }
- canvas.drawRect(mRectangles.get(i), mPaint);
-
- // border
- mPaint.setStyle(Paint.Style.STROKE);
- mPaint.setStrokeWidth(10);
- mPaint.setColor(Color.WHITE);
- canvas.drawRect(mRectangles.get(i), mPaint);
- }
- }
-
- @Override
- public boolean onTouchEvent(MotionEvent event) {
- int pointerIndex = event.getActionIndex();
- int pointerId = event.getPointerId(pointerIndex);
- int maskedAction = event.getActionMasked();
-
- boolean didImageChange = false;
- switch (maskedAction) {
- // Move each point from it's current point to the new point.
- case MotionEvent.ACTION_MOVE: {
- // Create an array to check for finger changes as multiple fingers may be on the
- // same tile. This two-pass algorithm records the overall difference before changing
- // the actual tiles.
- int[] notesChangedBy = new int[mRectangles.size()];
- for (int size = event.getPointerCount(), i = 0; i < size; i++) {
- PointF point = mLocationsOfFingers.get(event.getPointerId(i));
- if (point != null) {
- int prevIndex = getIndexFromLocation(point);
- point.x = event.getX(i);
- point.y = event.getY(i);
- int newIndex = getIndexFromLocation(point);
-
- if (newIndex != prevIndex) {
- if (prevIndex != -1) {
- notesChangedBy[prevIndex]--;
- }
- if (newIndex != -1) {
- notesChangedBy[newIndex]++;
- }
- }
- }
- }
-
- // Now go through the rectangles to see if they have changed
- for (int i = 0; i < mRectangles.size(); i++) {
- if (notesChangedBy[i] > 0) {
- mIsPressedPerRectangle[i] = true;
- mTileListener.onTileOn(i);
- didImageChange = true;
- } else if (notesChangedBy[i] < 0) {
- mIsPressedPerRectangle[i] = false;
- mTileListener.onTileOff(i);
- didImageChange = true;
- }
- }
- break;
- }
- // Add a new point when a location is pressed
- case MotionEvent.ACTION_DOWN:
- case MotionEvent.ACTION_POINTER_DOWN: {
- PointF f = new PointF();
- f.x = event.getX(pointerIndex);
- f.y = event.getY(pointerIndex);
- mLocationsOfFingers.put(pointerId, f);
- int curIndex = getIndexFromLocation(f);
- if (curIndex != -1) {
- mIsPressedPerRectangle[curIndex] = true;
- mTileListener.onTileOn(curIndex);
- didImageChange = true;
- }
- break;
- }
- // Remove a point when a location lifted or when there is an error
- case MotionEvent.ACTION_UP:
- case MotionEvent.ACTION_POINTER_UP:
- case MotionEvent.ACTION_CANCEL: {
- int curIndex = getIndexFromLocation(mLocationsOfFingers.get(event.getPointerId(pointerIndex)));
- if (curIndex != -1) {
- mIsPressedPerRectangle[curIndex] = false;
- mTileListener.onTileOff(curIndex);
- didImageChange = true;
- }
- mLocationsOfFingers.remove(pointerId);
- break;
- }
- }
-
- // Calling invalidate() will force onDraw() to be called
- if (didImageChange) {
- invalidate();
- }
-
- return true;
- }
-}
diff --git a/samples/SoundBoard/src/main/kotlin/com/google/oboe/samples/soundboard/MainActivity.kt b/samples/SoundBoard/src/main/kotlin/com/google/oboe/samples/soundboard/MainActivity.kt
new file mode 100644
index 00000000..a697bdb5
--- /dev/null
+++ b/samples/SoundBoard/src/main/kotlin/com/google/oboe/samples/soundboard/MainActivity.kt
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2021 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.oboe.samples.soundboard
+
+import android.app.Activity
+import android.content.Context
+import android.content.res.Configuration
+import android.graphics.Point
+import android.graphics.Rect
+import android.media.AudioManager
+import android.os.Build
+import android.os.Bundle
+import androidx.annotation.RequiresApi
+import androidx.appcompat.app.AppCompatActivity
+import kotlin.math.min
+
+class MainActivity : AppCompatActivity() {
+ private external fun startEngine(numSignals: Int): Long
+ private external fun stopEngine(engineHandle: Long)
+ private external fun native_setDefaultStreamValues(sampleRate: Int, framesPerBurst: Int)
+
+ companion object {
+ private const val DIMENSION_MIN_SIZE = 6
+ private const val DIMENSION_MAX_SIZE = 8
+ private var mNumColumns : Int = 0;
+ private var mNumRows : Int = 0;
+ private var mRectangles = ArrayList<Rect>()
+
+ private var mEngineHandle: Long = 0
+
+ // Used to load the 'native-lib' library on application startup.
+ init {
+ System.loadLibrary("soundboard")
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_main)
+ }
+
+ override fun onResume() {
+ super.onResume()
+ setup()
+ }
+
+ override fun onPause() {
+ stopEngine(mEngineHandle)
+ super.onPause()
+ }
+
+ private fun setup() {
+ setDefaultStreamValues(this)
+ calculateAndSetRectangles(this)
+ mEngineHandle = startEngine(mNumRows * mNumColumns)
+ createMusicTiles(this)
+ }
+
+ private fun setDefaultStreamValues(context: Context) {
+ val myAudioMgr = context.getSystemService(AUDIO_SERVICE) as AudioManager
+ val sampleRateStr = myAudioMgr.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE)
+ val defaultSampleRate = sampleRateStr.toInt()
+ val framesPerBurstStr =
+ myAudioMgr.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER)
+ val defaultFramesPerBurst = framesPerBurstStr.toInt()
+ native_setDefaultStreamValues(defaultSampleRate, defaultFramesPerBurst)
+ }
+
+ private fun calculateAndSetRectangles(context: Context) {
+ val width: Int
+ val height: Int
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ width = windowManager.currentWindowMetrics.bounds.width()
+ height = windowManager.currentWindowMetrics.bounds.height()
+ } else {
+ val size = Point()
+ windowManager.defaultDisplay.getRealSize(size)
+ height = size.y
+ width = size.x
+ }
+
+ if (height > width) {
+ mNumColumns = DIMENSION_MIN_SIZE
+ mNumRows = min(DIMENSION_MIN_SIZE * height / width, DIMENSION_MAX_SIZE)
+ } else {
+ mNumRows = DIMENSION_MIN_SIZE
+ mNumColumns = min(DIMENSION_MIN_SIZE * width / height, DIMENSION_MAX_SIZE)
+ }
+ val tileLength = min(height / mNumRows, width / mNumColumns)
+ val xStartLocation = (width - tileLength * mNumColumns) / 2
+ val yStartLocation = 0
+ mRectangles = ArrayList<Rect>()
+ for (i in 0 until mNumRows) {
+ for (j in 0 until mNumColumns) {
+ val rectangle = Rect(
+ xStartLocation + j * tileLength,
+ yStartLocation + i * tileLength,
+ xStartLocation + j * tileLength + tileLength,
+ yStartLocation + i * tileLength + tileLength
+ )
+ mRectangles.add(rectangle)
+ }
+ }
+ }
+
+ private fun createMusicTiles(context: Context) {
+ setContentView(MusicTileView(this, mRectangles, NoteListener(mEngineHandle),
+ ScreenChangeListener { setup() }))
+ }
+
+ class ScreenChangeListener(private var mFunc: () -> Unit) : MusicTileView.ConfigChangeListener {
+ override fun onConfigurationChanged() {
+ mFunc()
+ }
+ }
+
+}
diff --git a/samples/SoundBoard/src/main/kotlin/com/google/oboe/samples/soundboard/MusicTileView.kt b/samples/SoundBoard/src/main/kotlin/com/google/oboe/samples/soundboard/MusicTileView.kt
new file mode 100644
index 00000000..ac813450
--- /dev/null
+++ b/samples/SoundBoard/src/main/kotlin/com/google/oboe/samples/soundboard/MusicTileView.kt
@@ -0,0 +1,164 @@
+/*
+ * Copyright 2021 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.oboe.samples.soundboard
+
+import android.content.Context
+import android.content.res.Configuration
+import android.graphics.*
+import android.util.SparseArray
+import android.view.MotionEvent
+import android.view.View
+
+class MusicTileView(
+ context: Context?,
+ private val mRectangles: ArrayList<Rect>,
+ tileListener: TileListener,
+ configChangeListener: ConfigChangeListener
+) : View(context) {
+ private val mIsPressedPerRectangle: BooleanArray = BooleanArray(mRectangles.size)
+ private val mPaint: Paint = Paint()
+ private val mLocationsOfFingers: SparseArray<PointF> = SparseArray()
+ private val mTileListener: TileListener
+ private val mConfigChangeListener : ConfigChangeListener
+
+ interface ConfigChangeListener {
+ fun onConfigurationChanged()
+ }
+
+ interface TileListener {
+ fun onTileOn(index: Int)
+ fun onTileOff(index: Int)
+ }
+
+ private fun getIndexFromLocation(pointF: PointF): Int {
+ for (i in mRectangles.indices) {
+ if (pointF.x > mRectangles[i].left && pointF.x < mRectangles[i].right && pointF.y > mRectangles[i].top && pointF.y < mRectangles[i].bottom) {
+ return i
+ }
+ }
+ return -1
+ }
+
+ override fun onDraw(canvas: Canvas) {
+ for (i in mRectangles.indices) {
+ mPaint.style = Paint.Style.FILL
+ if (mIsPressedPerRectangle[i]) {
+ mPaint.color = Color.rgb(128, 0, 0)
+ } else {
+ mPaint.color = Color.BLACK
+ }
+ canvas.drawRect(mRectangles[i], mPaint)
+
+ // border
+ mPaint.style = Paint.Style.STROKE
+ mPaint.strokeWidth = 10f
+ mPaint.color = Color.WHITE
+ canvas.drawRect(mRectangles[i], mPaint)
+ }
+ }
+
+ override fun onTouchEvent(event: MotionEvent): Boolean {
+ val pointerIndex = event.actionIndex
+ val pointerId = event.getPointerId(pointerIndex)
+ val maskedAction = event.actionMasked
+ var didImageChange = false
+ when (maskedAction) {
+ MotionEvent.ACTION_MOVE -> {
+
+ // Create an array to check for finger changes as multiple fingers may be on the
+ // same tile. This two-pass algorithm records the overall difference before changing
+ // the actual tiles.
+ val notesChangedBy = IntArray(mRectangles.size)
+ run {
+ val size = event.pointerCount
+ var i = 0
+ while (i < size) {
+ val point = mLocationsOfFingers[event.getPointerId(i)]
+ if (point != null) {
+ val prevIndex = getIndexFromLocation(point)
+ point.x = event.getX(i)
+ point.y = event.getY(i)
+ val newIndex = getIndexFromLocation(point)
+ if (newIndex != prevIndex) {
+ if (prevIndex != -1) {
+ notesChangedBy[prevIndex]--
+ }
+ if (newIndex != -1) {
+ notesChangedBy[newIndex]++
+ }
+ }
+ }
+ i++
+ }
+ }
+
+ // Now go through the rectangles to see if they have changed
+ var i = 0
+ while (i < mRectangles.size) {
+ if (notesChangedBy[i] > 0) {
+ mIsPressedPerRectangle[i] = true
+ mTileListener.onTileOn(i)
+ didImageChange = true
+ } else if (notesChangedBy[i] < 0) {
+ mIsPressedPerRectangle[i] = false
+ mTileListener.onTileOff(i)
+ didImageChange = true
+ }
+ i++
+ }
+ }
+ MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN -> {
+ val f = PointF()
+ f.x = event.getX(pointerIndex)
+ f.y = event.getY(pointerIndex)
+ mLocationsOfFingers.put(pointerId, f)
+ val curIndex = getIndexFromLocation(f)
+ if (curIndex != -1) {
+ mIsPressedPerRectangle[curIndex] = true
+ mTileListener.onTileOn(curIndex)
+ didImageChange = true
+ }
+ }
+ MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP, MotionEvent.ACTION_CANCEL -> {
+ val curIndex =
+ getIndexFromLocation(mLocationsOfFingers[event.getPointerId(pointerIndex)])
+ if (curIndex != -1) {
+ mIsPressedPerRectangle[curIndex] = false
+ mTileListener.onTileOff(curIndex)
+ didImageChange = true
+ }
+ mLocationsOfFingers.remove(pointerId)
+ }
+ }
+
+ // Calling invalidate() will force onDraw() to be called
+ if (didImageChange) {
+ invalidate()
+ }
+ return true
+ }
+
+ override fun onConfigurationChanged(newConfig: Configuration) {
+ super.onConfigurationChanged(newConfig)
+ mConfigChangeListener.onConfigurationChanged()
+ }
+
+ init {
+ mTileListener = tileListener
+ mConfigChangeListener = configChangeListener
+ }
+}
diff --git a/samples/SoundBoard/src/main/java/com/google/oboe/samples/soundboard/NoteListener.java b/samples/SoundBoard/src/main/kotlin/com/google/oboe/samples/soundboard/NoteListener.kt
index e734ea3d..e660ed84 100644
--- a/samples/SoundBoard/src/main/java/com/google/oboe/samples/soundboard/NoteListener.java
+++ b/samples/SoundBoard/src/main/kotlin/com/google/oboe/samples/soundboard/NoteListener.kt
@@ -1,5 +1,3 @@
-package com.google.oboe.samples.soundboard;
-
/*
* Copyright 2021 The Android Open Source Project
*
@@ -16,21 +14,18 @@ package com.google.oboe.samples.soundboard;
* limitations under the License.
*/
-public class NoteListener implements MusicTileView.TileListener {
- private native void noteOn(long engineHandle, int noteIndex);
- private native void noteOff(long engineHandle, int noteIndex);
-
- long mEngineHandle;
+package com.google.oboe.samples.soundboard
- public NoteListener(long engineHandle) {
- mEngineHandle = engineHandle;
- }
+import com.google.oboe.samples.soundboard.MusicTileView.TileListener
- public void onTileOn(int index) {
- noteOn(mEngineHandle, index);
+class NoteListener(private var mEngineHandle: Long) : TileListener {
+ private external fun noteOn(engineHandle: Long, noteIndex: Int)
+ private external fun noteOff(engineHandle: Long, noteIndex: Int)
+ override fun onTileOn(index: Int) {
+ noteOn(mEngineHandle, index)
}
- public void onTileOff(int index) {
- noteOff(mEngineHandle, index);
+ override fun onTileOff(index: Int) {
+ noteOff(mEngineHandle, index)
}
}
diff --git a/samples/audio-device/build.gradle b/samples/audio-device/build.gradle
index 4f52c4e3..320888bf 100644
--- a/samples/audio-device/build.gradle
+++ b/samples/audio-device/build.gradle
@@ -1,10 +1,10 @@
apply plugin: 'com.android.library'
android {
- compileSdkVersion 33
+ compileSdkVersion 34
defaultConfig {
minSdkVersion 21
- targetSdkVersion 33
+ targetSdkVersion 34
}
buildTypes {
release {
diff --git a/samples/drumthumper/README.md b/samples/drumthumper/README.md
index d3e679b1..adfc6169 100644
--- a/samples/drumthumper/README.md
+++ b/samples/drumthumper/README.md
@@ -27,15 +27,13 @@ Secondarily, **DrumThumper** demonstrates:
* How to use the Oboe resampler to resample source audio to the device playback rate, and therefore not incur this overhead at playback time.
To keep things simple, **DrumThumper** specifically does not:
-* Does not support audio samples in other than 16-bit, mono PCM Samples. It does not support Stereo or different PCM formats.
+* Does not support audio samples in other than 16-bit PCM Samples. It does not support different PCM formats.
* Does not support non-WAV audio data (such as AIFF).
* Does not support compressed audio data.
**DrumThumper** now supports different sample rates for the source samples.
-If one wanted to extend **DrumThumper** to support Stereo samples:
-* The SampleSource class would need to be extended to understand Stereo SampleBuffer objects, it currently assumes Mono.
-* The OneShotSampleSource.mixAudio() method would need to have separate mixing logic for Stereo and Mono SampleSource.
+**DrumThumper** now supports mono and stereo sources.
## DrumThumper project structure
### Kotlin App Layer
@@ -64,3 +62,6 @@ An Android View subclass which implements the "trigger pad" UI widgets
## Native-interface (JNI)
* DrumPlayerJNI.cpp
This is where all the access to the native functionality is implemented.
+
+## Screenshots
+![drumthumper-screenshot](drumthumper-screenshot.png)
diff --git a/samples/drumthumper/build.gradle b/samples/drumthumper/build.gradle
index 17a11990..7a19c632 100644
--- a/samples/drumthumper/build.gradle
+++ b/samples/drumthumper/build.gradle
@@ -3,7 +3,7 @@ plugins {
id 'org.jetbrains.kotlin.android'
}
android {
- compileSdkVersion 33
+ compileSdkVersion 34
defaultConfig {
// Usually the applicationId follows the same scheme as the application package name,
@@ -13,7 +13,7 @@ android {
// who publishes using the application Id prefix of "com.plausiblesoftware".
applicationId "com.plausiblesoftware.drumthumper"
minSdkVersion 23
- targetSdkVersion 33
+ targetSdkVersion 34
versionCode 2
versionName "1.01"
diff --git a/samples/drumthumper/drumthumper-screenshot.png b/samples/drumthumper/drumthumper-screenshot.png
new file mode 100644
index 00000000..6fa0e8b0
--- /dev/null
+++ b/samples/drumthumper/drumthumper-screenshot.png
Binary files differ
diff --git a/samples/drumthumper/src/main/cpp/DrumPlayerJNI.cpp b/samples/drumthumper/src/main/cpp/DrumPlayerJNI.cpp
index 651dc61b..44f546f8 100644
--- a/samples/drumthumper/src/main/cpp/DrumPlayerJNI.cpp
+++ b/samples/drumthumper/src/main/cpp/DrumPlayerJNI.cpp
@@ -47,8 +47,6 @@ static SimpleMultiPlayer sDTPlayer;
JNIEXPORT void JNICALL Java_com_plausiblesoftware_drumthumper_DrumPlayer_setupAudioStreamNative(
JNIEnv* env, jobject, jint numChannels) {
__android_log_print(ANDROID_LOG_INFO, TAG, "%s", "init()");
-
- // we know in this case that the sample buffers are all 1-channel, 41K
sDTPlayer.setupAudioStream(numChannels);
}
@@ -74,8 +72,8 @@ JNIEXPORT void JNICALL Java_com_plausiblesoftware_drumthumper_DrumPlayer_teardow
/**
* Native (JNI) implementation of DrumPlayer.loadWavAssetNative()
*/
-JNIEXPORT jboolean JNICALL Java_com_plausiblesoftware_drumthumper_DrumPlayer_loadWavAssetNative(
- JNIEnv* env, jobject, jbyteArray bytearray, jint index, jfloat pan, jint channels) {
+JNIEXPORT void JNICALL Java_com_plausiblesoftware_drumthumper_DrumPlayer_loadWavAssetNative(
+ JNIEnv* env, jobject, jbyteArray bytearray, jint index, jfloat pan) {
int len = env->GetArrayLength (bytearray);
unsigned char* buf = new unsigned char[len];
@@ -86,7 +84,7 @@ JNIEXPORT jboolean JNICALL Java_com_plausiblesoftware_drumthumper_DrumPlayer_loa
WavStreamReader reader(&stream);
reader.parse();
- jboolean isFormatValid = reader.getNumChannels() == channels;
+ reader.getNumChannels();
SampleBuffer* sampleBuffer = new SampleBuffer();
sampleBuffer->loadSampleData(&reader);
@@ -95,8 +93,6 @@ JNIEXPORT jboolean JNICALL Java_com_plausiblesoftware_drumthumper_DrumPlayer_loa
sDTPlayer.addSampleSource(source, sampleBuffer);
delete[] buf;
-
- return isFormatValid;
}
/**
diff --git a/samples/drumthumper/src/main/java/com/plausibleaudio/drumthumper/DrumPlayer.kt b/samples/drumthumper/src/main/kotlin/com/plausibleaudio/drumthumper/DrumPlayer.kt
index 3b3a56ba..c47f6745 100644
--- a/samples/drumthumper/src/main/java/com/plausibleaudio/drumthumper/DrumPlayer.kt
+++ b/samples/drumthumper/src/main/kotlin/com/plausibleaudio/drumthumper/DrumPlayer.kt
@@ -24,9 +24,6 @@ class DrumPlayer {
// Sample attributes
val NUM_PLAY_CHANNELS: Int = 2 // The number of channels in the player Stream.
// Stereo Playback, set to 1 for Mono playback
- // This IS NOT the channel format of the source samples
- // (which must be mono).
- val NUM_SAMPLE_CHANNELS: Int = 1 // All WAV resource must be mono
// Sample Buffer IDs
val BASSDRUM: Int = 0
@@ -40,10 +37,10 @@ class DrumPlayer {
// initial pan position for each drum sample
val PAN_BASSDRUM: Float = 0f // Dead Center
- val PAN_SNAREDRUM: Float = 0.25f // A little Right
+ val PAN_SNAREDRUM: Float = -0.75f // Mostly Left
val PAN_CRASHCYMBAL: Float = -0.75f // Mostly Left
val PAN_RIDECYMBAL: Float = 1.0f // Hard Right
- val PAN_MIDTOM: Float = -0.75f // Mostly Left
+ val PAN_MIDTOM: Float = 0.25f // A little Right
val PAN_LOWTOM: Float = 0.75f // Mostly Right
val PAN_HIHATOPEN: Float = -1.0f // Hard Left
val PAN_HIHATCLOSED: Float = -1.0f // Hard Left
@@ -65,47 +62,40 @@ class DrumPlayer {
}
// asset-based samples
- fun loadWavAssets(assetMgr: AssetManager): Boolean {
- var allAssetsCorrect = true
- allAssetsCorrect = loadWavAsset(assetMgr, "KickDrum.wav", BASSDRUM, PAN_BASSDRUM) && allAssetsCorrect
- allAssetsCorrect = loadWavAsset(assetMgr, "SnareDrum.wav", SNAREDRUM, PAN_SNAREDRUM) && allAssetsCorrect
- allAssetsCorrect = loadWavAsset(assetMgr, "CrashCymbal.wav", CRASHCYMBAL, PAN_CRASHCYMBAL) && allAssetsCorrect
- allAssetsCorrect = loadWavAsset(assetMgr, "RideCymbal.wav", RIDECYMBAL, PAN_RIDECYMBAL) && allAssetsCorrect
- allAssetsCorrect = loadWavAsset(assetMgr, "MidTom.wav", MIDTOM, PAN_MIDTOM) && allAssetsCorrect
- allAssetsCorrect = loadWavAsset(assetMgr, "LowTom.wav", LOWTOM, PAN_LOWTOM) && allAssetsCorrect
- allAssetsCorrect = loadWavAsset(assetMgr, "HiHat_Open.wav", HIHATOPEN, PAN_HIHATOPEN) && allAssetsCorrect
- allAssetsCorrect = loadWavAsset(assetMgr, "HiHat_Closed.wav", HIHATCLOSED, PAN_HIHATCLOSED) && allAssetsCorrect
-
- return allAssetsCorrect
+ fun loadWavAssets(assetMgr: AssetManager) {
+ loadWavAsset(assetMgr, "KickDrum.wav", BASSDRUM, PAN_BASSDRUM)
+ loadWavAsset(assetMgr, "SnareDrum.wav", SNAREDRUM, PAN_SNAREDRUM)
+ loadWavAsset(assetMgr, "CrashCymbal.wav", CRASHCYMBAL, PAN_CRASHCYMBAL)
+ loadWavAsset(assetMgr, "RideCymbal.wav", RIDECYMBAL, PAN_RIDECYMBAL)
+ loadWavAsset(assetMgr, "MidTom.wav", MIDTOM, PAN_MIDTOM)
+ loadWavAsset(assetMgr, "LowTom.wav", LOWTOM, PAN_LOWTOM)
+ loadWavAsset(assetMgr, "HiHat_Open.wav", HIHATOPEN, PAN_HIHATOPEN)
+ loadWavAsset(assetMgr, "HiHat_Closed.wav", HIHATCLOSED, PAN_HIHATCLOSED)
}
fun unloadWavAssets() {
unloadWavAssetsNative()
}
- private fun loadWavAsset(assetMgr: AssetManager, assetName: String, index: Int, pan: Float) : Boolean {
- var returnVal = false
+ private fun loadWavAsset(assetMgr: AssetManager, assetName: String, index: Int, pan: Float) {
try {
val assetFD = assetMgr.openFd(assetName)
val dataStream = assetFD.createInputStream()
val dataLen = assetFD.getLength().toInt()
val dataBytes = ByteArray(dataLen)
dataStream.read(dataBytes, 0, dataLen)
- returnVal = loadWavAssetNative(dataBytes, index, pan, NUM_SAMPLE_CHANNELS)
+ loadWavAssetNative(dataBytes, index, pan)
assetFD.close()
} catch (ex: IOException) {
Log.i(TAG, "IOException$ex")
}
-
- return returnVal
}
private external fun setupAudioStreamNative(numChannels: Int)
private external fun startAudioStreamNative()
private external fun teardownAudioStreamNative()
- private external fun loadWavAssetNative(
- wavBytes: ByteArray, index: Int, pan: Float, channels: Int) : Boolean
+ private external fun loadWavAssetNative(wavBytes: ByteArray, index: Int, pan: Float)
private external fun unloadWavAssetsNative()
external fun trigger(drumIndex: Int)
diff --git a/samples/drumthumper/src/main/java/com/plausibleaudio/drumthumper/DrumThumperActivity.kt b/samples/drumthumper/src/main/kotlin/com/plausibleaudio/drumthumper/DrumThumperActivity.kt
index 1ec49059..ffb30308 100644
--- a/samples/drumthumper/src/main/java/com/plausibleaudio/drumthumper/DrumThumperActivity.kt
+++ b/samples/drumthumper/src/main/kotlin/com/plausibleaudio/drumthumper/DrumThumperActivity.kt
@@ -168,15 +168,8 @@ class DrumThumperActivity : AppCompatActivity(),
mDrumPlayer.setupAudioStream()
- val allAssetsValid = mDrumPlayer.loadWavAssets(getAssets())
-
- if (!allAssetsValid) {
- // show toast
- val toast = Toast.makeText(this,
- "One or more audio assets has an incorrect format and may not play correctly",
- Toast.LENGTH_LONG)
- toast.show()
- }
+ mDrumPlayer.loadWavAssets(getAssets())
+
mDrumPlayer.startAudioStream()
if (mUseDeviceChangeFallback) {
diff --git a/samples/drumthumper/src/main/java/com/plausibleaudio/drumthumper/TriggerPad.kt b/samples/drumthumper/src/main/kotlin/com/plausibleaudio/drumthumper/TriggerPad.kt
index 98b1b5ee..98b1b5ee 100644
--- a/samples/drumthumper/src/main/java/com/plausibleaudio/drumthumper/TriggerPad.kt
+++ b/samples/drumthumper/src/main/kotlin/com/plausibleaudio/drumthumper/TriggerPad.kt
diff --git a/samples/hello-oboe/build.gradle b/samples/hello-oboe/build.gradle
index 29b6fc26..59b0b188 100644
--- a/samples/hello-oboe/build.gradle
+++ b/samples/hello-oboe/build.gradle
@@ -1,12 +1,12 @@
apply plugin: 'com.android.application'
android {
- compileSdkVersion 33
+ compileSdkVersion 34
defaultConfig {
applicationId 'com.google.oboe.samples.hellooboe'
minSdkVersion 21
- targetSdkVersion 33
+ targetSdkVersion 34
versionCode 1
versionName '1.0'
externalNativeBuild {
diff --git a/samples/iolib/build.gradle b/samples/iolib/build.gradle
index d0571a92..266c9789 100644
--- a/samples/iolib/build.gradle
+++ b/samples/iolib/build.gradle
@@ -1,11 +1,11 @@
apply plugin: 'com.android.library'
android {
- compileSdkVersion 33
+ compileSdkVersion 34
defaultConfig {
minSdkVersion 21
- targetSdkVersion 33
+ targetSdkVersion 34
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles 'consumer-rules.pro'
diff --git a/samples/iolib/src/main/cpp/player/OneShotSampleSource.cpp b/samples/iolib/src/main/cpp/player/OneShotSampleSource.cpp
index 22f6293b..568d76f2 100644
--- a/samples/iolib/src/main/cpp/player/OneShotSampleSource.cpp
+++ b/samples/iolib/src/main/cpp/player/OneShotSampleSource.cpp
@@ -23,31 +23,44 @@
namespace iolib {
void OneShotSampleSource::mixAudio(float* outBuff, int numChannels, int32_t numFrames) {
- int32_t numSampleFrames = mSampleBuffer->getNumSampleFrames();
+ int32_t numSamples = mSampleBuffer->getNumSamples();
+ int32_t sampleChannels = mSampleBuffer->getProperties().channelCount;
+ int32_t samplesLeft = numSamples - mCurSampleIndex;
int32_t numWriteFrames = mIsPlaying
- ? std::min(numFrames, numSampleFrames - mCurFrameIndex)
+ ? std::min(numFrames, samplesLeft / sampleChannels)
: 0;
if (numWriteFrames != 0) {
- // Mix in the samples
-
- // investigate unrolling these loops...
const float* data = mSampleBuffer->getSampleData();
- if (numChannels == 1) {
- // MONO output
+ if ((sampleChannels == 1) && (numChannels == 1)) {
+ // MONO output from MONO samples
+ for (int32_t frameIndex = 0; frameIndex < numWriteFrames; frameIndex++) {
+ outBuff[frameIndex] += data[mCurSampleIndex++] * mGain;
+ }
+ } else if ((sampleChannels == 1) && (numChannels == 2)) {
+ // STEREO output from MONO samples
+ int dstSampleIndex = 0;
+ for (int32_t frameIndex = 0; frameIndex < numWriteFrames; frameIndex++) {
+ outBuff[dstSampleIndex++] += data[mCurSampleIndex] * mLeftGain;
+ outBuff[dstSampleIndex++] += data[mCurSampleIndex++] * mRightGain;
+ }
+ } else if ((sampleChannels == 2) && (numChannels == 1)) {
+ // MONO output from STEREO samples
+ int dstSampleIndex = 0;
for (int32_t frameIndex = 0; frameIndex < numWriteFrames; frameIndex++) {
- outBuff[frameIndex] += data[mCurFrameIndex++] * mGain;
+ outBuff[dstSampleIndex++] += data[mCurSampleIndex++] * mLeftGain +
+ data[mCurSampleIndex++] * mRightGain;
}
- } else if (numChannels == 2) {
- // STEREO output
+ } else if ((sampleChannels == 2) && (numChannels == 2)) {
+ // STEREO output from STEREO samples
int dstSampleIndex = 0;
for (int32_t frameIndex = 0; frameIndex < numWriteFrames; frameIndex++) {
- outBuff[dstSampleIndex++] += data[mCurFrameIndex] * mLeftGain;
- outBuff[dstSampleIndex++] += data[mCurFrameIndex++] * mRightGain;
+ outBuff[dstSampleIndex++] += data[mCurSampleIndex++] * mLeftGain;
+ outBuff[dstSampleIndex++] += data[mCurSampleIndex++] * mRightGain;
}
}
- if (mCurFrameIndex >= numSampleFrames) {
+ if (mCurSampleIndex >= numSamples) {
mIsPlaying = false;
}
}
diff --git a/samples/iolib/src/main/cpp/player/SampleBuffer.cpp b/samples/iolib/src/main/cpp/player/SampleBuffer.cpp
index 2ab065d1..f3694b3c 100644
--- a/samples/iolib/src/main/cpp/player/SampleBuffer.cpp
+++ b/samples/iolib/src/main/cpp/player/SampleBuffer.cpp
@@ -26,7 +26,6 @@ using namespace RESAMPLER_OUTER_NAMESPACE::resampler;
namespace iolib {
void SampleBuffer::loadSampleData(parselib::WavStreamReader* reader) {
- // Although we read this in, at this time we know a-priori that the data is mono
mAudioProperties.channelCount = reader->getNumChannels();
mAudioProperties.sampleRate = reader->getSampleRate();
@@ -50,19 +49,19 @@ class ResampleBlock {
public:
int32_t mSampleRate;
float* mBuffer;
- int32_t mNumFrames;
+ int32_t mNumSamples;
};
void resampleData(const ResampleBlock& input, ResampleBlock* output, int numChannels) {
// Calculate output buffer size
double temp =
- ((double)input.mNumFrames * (double)output->mSampleRate) / (double)input.mSampleRate;
+ ((double)input.mNumSamples * (double)output->mSampleRate) / (double)input.mSampleRate;
// round up
- int32_t numOutFrames = (int32_t)(temp + 0.5);
+ int32_t numOutFramesAllocated = (int32_t)(temp + 0.5);
// We iterate thousands of times through the loop. Roundoff error could accumulate
// so add a few more frames for padding
- numOutFrames += 8;
+ numOutFramesAllocated += 8;
MultiChannelResampler *resampler = MultiChannelResampler::make(
numChannels, // channel count
@@ -71,23 +70,23 @@ void resampleData(const ResampleBlock& input, ResampleBlock* output, int numChan
MultiChannelResampler::Quality::Medium); // conversion quality
float *inputBuffer = input.mBuffer;; // multi-channel buffer to be consumed
- float *outputBuffer = new float[numOutFrames]; // multi-channel buffer to be filled
+ float *outputBuffer = new float[numOutFramesAllocated]; // multi-channel buffer to be filled
output->mBuffer = outputBuffer;
- int numOutputFrames = 0;
- int inputFramesLeft = input.mNumFrames;
- while (inputFramesLeft > 0) {
+ int numOutputSamples = 0;
+ int inputSamplesLeft = input.mNumSamples;
+ while ((inputSamplesLeft > 0) && (numOutputSamples < numOutFramesAllocated)) {
if(resampler->isWriteNeeded()) {
resampler->writeNextFrame(inputBuffer);
inputBuffer += numChannels;
- inputFramesLeft--;
+ inputSamplesLeft -= numChannels;
} else {
resampler->readNextFrame(outputBuffer);
outputBuffer += numChannels;
- numOutputFrames++;
+ numOutputSamples += numChannels;
}
}
- output->mNumFrames = numOutputFrames;
+ output->mNumSamples = numOutputSamples;
delete resampler;
}
@@ -100,7 +99,7 @@ void SampleBuffer::resampleData(int sampleRate) {
ResampleBlock inputBlock;
inputBlock.mBuffer = mSampleData;
- inputBlock.mNumFrames = mNumSamples;
+ inputBlock.mNumSamples = mNumSamples;
inputBlock.mSampleRate = mAudioProperties.sampleRate;
ResampleBlock outputBlock;
@@ -112,7 +111,7 @@ void SampleBuffer::resampleData(int sampleRate) {
// install the resampled data
mSampleData = outputBlock.mBuffer;
- mNumSamples = outputBlock.mNumFrames;
+ mNumSamples = outputBlock.mNumSamples;
mAudioProperties.sampleRate = outputBlock.mSampleRate;
}
diff --git a/samples/iolib/src/main/cpp/player/SampleBuffer.h b/samples/iolib/src/main/cpp/player/SampleBuffer.h
index c92ee78f..7cc25c1f 100644
--- a/samples/iolib/src/main/cpp/player/SampleBuffer.h
+++ b/samples/iolib/src/main/cpp/player/SampleBuffer.h
@@ -43,7 +43,7 @@ public:
virtual AudioProperties getProperties() const { return mAudioProperties; }
float* getSampleData() { return mSampleData; }
- int32_t getNumSampleFrames() { return mNumSamples; }
+ int32_t getNumSamples() { return mNumSamples; }
protected:
AudioProperties mAudioProperties;
diff --git a/samples/iolib/src/main/cpp/player/SampleSource.h b/samples/iolib/src/main/cpp/player/SampleSource.h
index 69dc57bf..80be39df 100644
--- a/samples/iolib/src/main/cpp/player/SampleSource.h
+++ b/samples/iolib/src/main/cpp/player/SampleSource.h
@@ -39,13 +39,13 @@ public:
static constexpr float PAN_CENTER = 0.0f;
SampleSource(SampleBuffer *sampleBuffer, float pan)
- : mSampleBuffer(sampleBuffer), mCurFrameIndex(0), mIsPlaying(false), mGain(1.0f) {
+ : mSampleBuffer(sampleBuffer), mCurSampleIndex(0), mIsPlaying(false), mGain(1.0f) {
setPan(pan);
}
virtual ~SampleSource() {}
- void setPlayMode() { mCurFrameIndex = 0; mIsPlaying = true; }
- void setStopMode() { mIsPlaying = false; mCurFrameIndex = 0; }
+ void setPlayMode() { mCurSampleIndex = 0; mIsPlaying = true; }
+ void setStopMode() { mIsPlaying = false; mCurSampleIndex = 0; }
bool isPlaying() { return mIsPlaying; }
@@ -76,7 +76,7 @@ public:
protected:
SampleBuffer *mSampleBuffer;
- int32_t mCurFrameIndex;
+ int32_t mCurSampleIndex;
bool mIsPlaying;
diff --git a/samples/iolib/src/main/cpp/player/SimpleMultiPlayer.cpp b/samples/iolib/src/main/cpp/player/SimpleMultiPlayer.cpp
index aa133bee..4948d88a 100644
--- a/samples/iolib/src/main/cpp/player/SimpleMultiPlayer.cpp
+++ b/samples/iolib/src/main/cpp/player/SimpleMultiPlayer.cpp
@@ -34,11 +34,12 @@ namespace iolib {
constexpr int32_t kBufferSizeInBursts = 2; // Use 2 bursts as the buffer size (double buffer)
SimpleMultiPlayer::SimpleMultiPlayer()
- : mChannelCount(0), mOutputReset(false)
+ : mChannelCount(0), mOutputReset(false), mSampleRate(0), mNumSampleBuffers(0)
{}
-DataCallbackResult SimpleMultiPlayer::onAudioReady(AudioStream *oboeStream, void *audioData,
- int32_t numFrames) {
+DataCallbackResult SimpleMultiPlayer::MyDataCallback::onAudioReady(AudioStream *oboeStream,
+ void *audioData,
+ int32_t numFrames) {
StreamState streamState = oboeStream->getState();
if (streamState != StreamState::Open && streamState != StreamState::Started) {
@@ -48,40 +49,42 @@ DataCallbackResult SimpleMultiPlayer::onAudioReady(AudioStream *oboeStream, void
__android_log_print(ANDROID_LOG_ERROR, TAG, " streamState::Disconnected");
}
- memset(audioData, 0, static_cast<size_t>(numFrames) * static_cast<size_t>(mChannelCount)
- * sizeof(float));
+ memset(audioData, 0, static_cast<size_t>(numFrames) * static_cast<size_t>
+ (mParent->mChannelCount) * sizeof(float));
// OneShotSampleSource* sources = mSampleSources.get();
- for(int32_t index = 0; index < mNumSampleBuffers; index++) {
- if (mSampleSources[index]->isPlaying()) {
- mSampleSources[index]->mixAudio((float*)audioData, mChannelCount, numFrames);
+ for(int32_t index = 0; index < mParent->mNumSampleBuffers; index++) {
+ if (mParent->mSampleSources[index]->isPlaying()) {
+ mParent->mSampleSources[index]->mixAudio((float*)audioData, mParent->mChannelCount,
+ numFrames);
}
}
return DataCallbackResult::Continue;
}
-void SimpleMultiPlayer::onErrorAfterClose(AudioStream *oboeStream, Result error) {
+void SimpleMultiPlayer::MyErrorCallback::onErrorAfterClose(AudioStream *oboeStream, Result error) {
__android_log_print(ANDROID_LOG_INFO, TAG, "==== onErrorAfterClose() error:%d", error);
- resetAll();
- if (openStream() && startStream()) {
- mOutputReset = true;
+ mParent->resetAll();
+ if (mParent->openStream() && mParent->startStream()) {
+ mParent->mOutputReset = true;
}
}
-void SimpleMultiPlayer::onErrorBeforeClose(AudioStream *, Result error) {
- __android_log_print(ANDROID_LOG_INFO, TAG, "==== onErrorBeforeClose() error:%d", error);
-}
-
bool SimpleMultiPlayer::openStream() {
__android_log_print(ANDROID_LOG_INFO, TAG, "openStream()");
+ // Use shared_ptr to prevent use of a deleted callback.
+ mDataCallback = std::make_shared<MyDataCallback>(this);
+ mErrorCallback = std::make_shared<MyErrorCallback>(this);
+
// Create an audio stream
AudioStreamBuilder builder;
builder.setChannelCount(mChannelCount);
// we will resample source data to device rate, so take default sample rate
- builder.setCallback(this);
+ builder.setDataCallback(mDataCallback);
+ builder.setErrorCallback(mErrorCallback);
builder.setPerformanceMode(PerformanceMode::LowLatency);
builder.setSharingMode(SharingMode::Exclusive);
builder.setSampleRateConversionQuality(SampleRateConversionQuality::Medium);
@@ -113,16 +116,31 @@ bool SimpleMultiPlayer::openStream() {
}
bool SimpleMultiPlayer::startStream() {
- Result result = mAudioStream->requestStart();
- if (result != Result::OK){
- __android_log_print(
- ANDROID_LOG_ERROR,
- TAG,
- "requestStart failed. Error: %s", convertToText(result));
- return false;
+ int tryCount = 0;
+ while (tryCount < 3) {
+ bool wasOpenSuccessful = true;
+ // Assume that apenStream() was called successfully before startStream() call.
+ if (tryCount > 0) {
+ usleep(20 * 1000); // Sleep between tries to give the system time to settle.
+ wasOpenSuccessful = openStream(); // Try to open the stream again after the first try.
+ }
+ if (wasOpenSuccessful) {
+ Result result = mAudioStream->requestStart();
+ if (result != Result::OK){
+ __android_log_print(
+ ANDROID_LOG_ERROR,
+ TAG,
+ "requestStart failed. Error: %s", convertToText(result));
+ mAudioStream->close();
+ mAudioStream.reset();
+ } else {
+ return true;
+ }
+ }
+ tryCount++;
}
- return true;
+ return false;
}
void SimpleMultiPlayer::setupAudioStream(int32_t channelCount) {
diff --git a/samples/iolib/src/main/cpp/player/SimpleMultiPlayer.h b/samples/iolib/src/main/cpp/player/SimpleMultiPlayer.h
index 2f2307ea..b388b8f3 100644
--- a/samples/iolib/src/main/cpp/player/SimpleMultiPlayer.h
+++ b/samples/iolib/src/main/cpp/player/SimpleMultiPlayer.h
@@ -14,8 +14,8 @@
* limitations under the License.
*/
-#ifndef _PLAYER_SIMIPLEMULTIPLAYER_H_
-#define _PLAYER_SIMIPLEMULTIPLAYER_H_
+#ifndef _PLAYER_SIMPLEMULTIPLAYER_H_
+#define _PLAYER_SIMPLEMULTIPLAYER_H_
#include <vector>
@@ -26,21 +26,13 @@
namespace iolib {
-typedef unsigned char byte; // an 8-bit unsigned value
-
/**
* A simple streaming player for multiple SampleBuffers.
*/
-class SimpleMultiPlayer : public oboe::AudioStreamCallback {
+class SimpleMultiPlayer {
public:
SimpleMultiPlayer();
- // Inherited from oboe::AudioStreamCallback
- oboe::DataCallbackResult onAudioReady(oboe::AudioStream *oboeStream, void *audioData,
- int32_t numFrames) override;
- virtual void onErrorAfterClose(oboe::AudioStream *oboeStream, oboe::Result error) override;
- virtual void onErrorBeforeClose(oboe::AudioStream * oboeStream, oboe::Result error) override;
-
void setupAudioStream(int32_t channelCount);
void teardownAudioStream();
@@ -51,7 +43,7 @@ public:
// Wave Sample Loading...
/**
- * Adds the SampleSource/Samplebuffer pair to the list of source channels.
+ * Adds the SampleSource/SampleBuffer pair to the list of source channels.
* Transfers ownership of those objects so that they can be deleted/unloaded.
* The indexes associated with each source channel is the order in which they
* are added.
@@ -77,6 +69,32 @@ public:
float getGain(int index);
private:
+ class MyDataCallback : public oboe::AudioStreamDataCallback {
+ public:
+ MyDataCallback(SimpleMultiPlayer *parent) : mParent(parent) {}
+
+ oboe::DataCallbackResult onAudioReady(
+ oboe::AudioStream *audioStream,
+ void *audioData,
+ int32_t numFrames) override;
+
+ private:
+ SimpleMultiPlayer *mParent;
+ };
+
+ class MyErrorCallback : public oboe::AudioStreamErrorCallback {
+ public:
+ MyErrorCallback(SimpleMultiPlayer *parent) : mParent(parent) {}
+
+ virtual ~MyErrorCallback() {
+ }
+
+ void onErrorAfterClose(oboe::AudioStream *oboeStream, oboe::Result error) override;
+
+ private:
+ SimpleMultiPlayer *mParent;
+ };
+
// Oboe Audio Stream
std::shared_ptr<oboe::AudioStream> mAudioStream;
@@ -90,7 +108,10 @@ private:
std::vector<SampleSource*> mSampleSources;
bool mOutputReset;
+
+ std::shared_ptr<MyDataCallback> mDataCallback;
+ std::shared_ptr<MyErrorCallback> mErrorCallback;
};
}
-#endif //_PLAYER_SIMIPLEMULTIPLAYER_H_
+#endif //_PLAYER_SIMPLEMULTIPLAYER_H_
diff --git a/samples/minimaloboe/README.md b/samples/minimaloboe/README.md
index 4d080edd..cd8f68e3 100644
--- a/samples/minimaloboe/README.md
+++ b/samples/minimaloboe/README.md
@@ -14,3 +14,6 @@ Oboe is called through an external native function.
This app uses shared_ptr for passing callbacks to Oboe.
When the stream is disconnected, it starts a new stream.
+
+## Screenshots
+![minimaloboe-screenshot](minimaloboe-screenshot.png)
diff --git a/samples/minimaloboe/minimaloboe-screenshot.png b/samples/minimaloboe/minimaloboe-screenshot.png
new file mode 100644
index 00000000..155e3610
--- /dev/null
+++ b/samples/minimaloboe/minimaloboe-screenshot.png
Binary files differ
diff --git a/samples/minimaloboe/src/main/java/com/example/minimaloboe/AudioPlayer.kt b/samples/minimaloboe/src/main/kotlin/com/example/minimaloboe/AudioPlayer.kt
index 4871d741..445e3644 100644
--- a/samples/minimaloboe/src/main/java/com/example/minimaloboe/AudioPlayer.kt
+++ b/samples/minimaloboe/src/main/kotlin/com/example/minimaloboe/AudioPlayer.kt
@@ -71,8 +71,8 @@ object AudioPlayer : DefaultLifecycleObserver {
}
override fun onStop(owner: LifecycleOwner) {
- super.onStop(owner)
setPlaybackEnabled(false)
+ super.onStop(owner)
}
private external fun startAudioStreamNative(): Int
diff --git a/samples/minimaloboe/src/main/java/com/example/minimaloboe/MainActivity.kt b/samples/minimaloboe/src/main/kotlin/com/example/minimaloboe/MainActivity.kt
index edb29011..edb29011 100644
--- a/samples/minimaloboe/src/main/java/com/example/minimaloboe/MainActivity.kt
+++ b/samples/minimaloboe/src/main/kotlin/com/example/minimaloboe/MainActivity.kt
diff --git a/samples/minimaloboe/src/main/java/com/example/minimaloboe/ui/theme/Color.kt b/samples/minimaloboe/src/main/kotlin/com/example/minimaloboe/ui/theme/Color.kt
index c4f8298b..c4f8298b 100644
--- a/samples/minimaloboe/src/main/java/com/example/minimaloboe/ui/theme/Color.kt
+++ b/samples/minimaloboe/src/main/kotlin/com/example/minimaloboe/ui/theme/Color.kt
diff --git a/samples/minimaloboe/src/main/java/com/example/minimaloboe/ui/theme/Shape.kt b/samples/minimaloboe/src/main/kotlin/com/example/minimaloboe/ui/theme/Shape.kt
index ca2815d9..ca2815d9 100644
--- a/samples/minimaloboe/src/main/java/com/example/minimaloboe/ui/theme/Shape.kt
+++ b/samples/minimaloboe/src/main/kotlin/com/example/minimaloboe/ui/theme/Shape.kt
diff --git a/samples/minimaloboe/src/main/java/com/example/minimaloboe/ui/theme/Theme.kt b/samples/minimaloboe/src/main/kotlin/com/example/minimaloboe/ui/theme/Theme.kt
index 28c66a0e..28c66a0e 100644
--- a/samples/minimaloboe/src/main/java/com/example/minimaloboe/ui/theme/Theme.kt
+++ b/samples/minimaloboe/src/main/kotlin/com/example/minimaloboe/ui/theme/Theme.kt
diff --git a/samples/minimaloboe/src/main/java/com/example/minimaloboe/ui/theme/Type.kt b/samples/minimaloboe/src/main/kotlin/com/example/minimaloboe/ui/theme/Type.kt
index b391730f..b391730f 100644
--- a/samples/minimaloboe/src/main/java/com/example/minimaloboe/ui/theme/Type.kt
+++ b/samples/minimaloboe/src/main/kotlin/com/example/minimaloboe/ui/theme/Type.kt
diff --git a/samples/parselib/build.gradle b/samples/parselib/build.gradle
index 58b2190d..c593a5de 100644
--- a/samples/parselib/build.gradle
+++ b/samples/parselib/build.gradle
@@ -1,11 +1,11 @@
apply plugin: 'com.android.library'
android {
- compileSdkVersion 33
+ compileSdkVersion 34
defaultConfig {
minSdkVersion 21
- targetSdkVersion 33
+ targetSdkVersion 34
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
diff --git a/samples/shared/Mixer.h b/samples/shared/Mixer.h
index c91766e6..e73a29b8 100644
--- a/samples/shared/Mixer.h
+++ b/samples/shared/Mixer.h
@@ -20,9 +20,6 @@
#include <array>
#include "IRenderableAudio.h"
-constexpr int32_t kBufferSize = 192*10; // Temporary buffer is used for mixing
-constexpr uint8_t kMaxTracks = 100;
-
/**
* A Mixer object which sums the output from multiple tracks into a single output. The number of
* input channels on each track must match the number of output channels (default 1=mono). This can
@@ -34,35 +31,38 @@ class Mixer : public IRenderableAudio {
public:
void renderAudio(float *audioData, int32_t numFrames) {
+ int numSamples = numFrames * mChannelCount;
+ if (numSamples > mBufferSize) {
+ mMixingBuffer = std::make_unique<float[]>(numSamples);
+ mBufferSize = numSamples;
+ }
+
// Zero out the incoming container array
- memset(audioData, 0, sizeof(float) * numFrames * mChannelCount);
+ memset(audioData, 0, sizeof(float) * numSamples);
- for (int i = 0; i < mNextFreeTrackIndex; ++i) {
- mTracks[i]->renderAudio(mixingBuffer, numFrames);
+ for (int i = 0; i < mTracks.size(); ++i) {
+ mTracks[i]->renderAudio(mMixingBuffer.get(), numFrames);
- for (int j = 0; j < numFrames * mChannelCount; ++j) {
- audioData[j] += mixingBuffer[j];
+ for (int j = 0; j < numSamples; ++j) {
+ audioData[j] += mMixingBuffer[j];
}
}
}
void addTrack(IRenderableAudio *renderer){
- mTracks[mNextFreeTrackIndex++] = renderer;
+ mTracks.push_back(renderer);
}
void setChannelCount(int32_t channelCount){ mChannelCount = channelCount; }
void removeAllTracks(){
- for (int i = 0; i < mNextFreeTrackIndex; i++){
- mTracks[i] = nullptr;
- }
- mNextFreeTrackIndex = 0;
+ mTracks.clear();
}
private:
- float mixingBuffer[kBufferSize];
- std::array<IRenderableAudio*, kMaxTracks> mTracks;
- uint8_t mNextFreeTrackIndex = 0;
+ int32_t mBufferSize = 0;
+ std::unique_ptr<float[]> mMixingBuffer;
+ std::vector<IRenderableAudio*> mTracks;
int32_t mChannelCount = 1; // Default to mono
};
diff --git a/samples/shared/SynthSound.h b/samples/shared/SynthSound.h
index 43fc5513..01acd4de 100644
--- a/samples/shared/SynthSound.h
+++ b/samples/shared/SynthSound.h
@@ -91,12 +91,12 @@ public:
};
private:
- std::atomic<bool> mTrigger;
+ std::atomic<bool> mTrigger { false };
float mMasterAmplitude = 0.0f;
- std::atomic<float> mAmplitudeScaler;
+ std::atomic<float> mAmplitudeScaler { 0.0f };
std::array<std::atomic<float>, kNumSineWaves> mAmplitudes;
float mPhase = 0.0f;
- std::atomic<float> mPhaseIncrement;
+ std::atomic<float> mPhaseIncrement { 0 };
std::atomic<float> mFrequency { kDefaultFrequency };
std::atomic<int32_t> mSampleRate { kDefaultSampleRate };
void updatePhaseIncrement(){
diff --git a/src/aaudio/AAudioExtensions.h b/src/aaudio/AAudioExtensions.h
index b3653c02..eed73f37 100644
--- a/src/aaudio/AAudioExtensions.h
+++ b/src/aaudio/AAudioExtensions.h
@@ -41,7 +41,7 @@ typedef struct AAudioStreamStruct AAudioStream;
* Call some AAudio test routines that are not part of the normal API.
*/
class AAudioExtensions {
-public:
+private: // Because it is a singleton. Call getInstance() instead.
AAudioExtensions() {
int32_t policy = getIntegerProperty("aaudio.mmap_policy", 0);
mMMapSupported = isPolicyEnabled(policy);
@@ -50,6 +50,7 @@ public:
mMMapExclusiveSupported = isPolicyEnabled(policy);
}
+public:
static bool isPolicyEnabled(int32_t policy) {
return (policy == AAUDIO_POLICY_AUTO || policy == AAUDIO_POLICY_ALWAYS);
}
@@ -88,7 +89,7 @@ public:
if (loadSymbols()) return false;
if (mAAudio_getMMapPolicy == nullptr) return false;
int32_t policy = mAAudio_getMMapPolicy();
- return isPolicyEnabled(policy);
+ return (policy == Unspecified) ? mMMapSupported : isPolicyEnabled(policy);
}
bool isMMapSupported() {
diff --git a/src/aaudio/AAudioLoader.cpp b/src/aaudio/AAudioLoader.cpp
index 82595fd9..213a6ef9 100644
--- a/src/aaudio/AAudioLoader.cpp
+++ b/src/aaudio/AAudioLoader.cpp
@@ -83,6 +83,14 @@ int AAudioLoader::open() {
builder_setSessionId = load_V_PBI("AAudioStreamBuilder_setSessionId");
}
+ if (getSdkVersion() >= __ANDROID_API_Q__){
+ builder_setAllowedCapturePolicy = load_V_PBI("AAudioStreamBuilder_setAllowedCapturePolicy");
+ }
+
+ if (getSdkVersion() >= __ANDROID_API_R__){
+ builder_setPrivacySensitive = load_V_PBO("AAudioStreamBuilder_setPrivacySensitive");
+ }
+
if (getSdkVersion() >= __ANDROID_API_S__){
builder_setPackageName = load_V_PBCPH("AAudioStreamBuilder_setPackageName");
builder_setAttributionTag = load_V_PBCPH("AAudioStreamBuilder_setAttributionTag");
@@ -90,6 +98,8 @@ int AAudioLoader::open() {
if (getSdkVersion() >= __ANDROID_API_S_V2__) {
builder_setChannelMask = load_V_PBU("AAudioStreamBuilder_setChannelMask");
+ builder_setIsContentSpatialized = load_V_PBO("AAudioStreamBuilder_setIsContentSpatialized");
+ builder_setSpatializationBehavior = load_V_PBI("AAudioStreamBuilder_setSpatializationBehavior");
}
builder_delete = load_I_PB("AAudioStreamBuilder_delete");
@@ -112,6 +122,10 @@ int AAudioLoader::open() {
stream_getChannelCount = load_I_PS("AAudioStream_getSamplesPerFrame");
}
+ if (getSdkVersion() >= __ANDROID_API_R__) {
+ stream_release = load_I_PS("AAudioStream_release");
+ }
+
stream_close = load_I_PS("AAudioStream_close");
stream_getBufferSize = load_I_PS("AAudioStream_getBufferSizeInFrames");
@@ -143,9 +157,26 @@ int AAudioLoader::open() {
stream_getSessionId = load_I_PS("AAudioStream_getSessionId");
}
+ if (getSdkVersion() >= __ANDROID_API_Q__){
+ stream_getAllowedCapturePolicy = load_I_PS("AAudioStream_getAllowedCapturePolicy");
+ }
+
+ if (getSdkVersion() >= __ANDROID_API_R__){
+ stream_isPrivacySensitive = load_O_PS("AAudioStream_isPrivacySensitive");
+ }
+
if (getSdkVersion() >= __ANDROID_API_S_V2__) {
stream_getChannelMask = load_U_PS("AAudioStream_getChannelMask");
+ stream_isContentSpatialized = load_O_PS("AAudioStream_isContentSpatialized");
+ stream_getSpatializationBehavior = load_I_PS("AAudioStream_getSpatializationBehavior");
+ }
+
+ if (getSdkVersion() >= __ANDROID_API_U__) {
+ stream_getHardwareChannelCount = load_I_PS("AAudioStream_getHardwareChannelCount");
+ stream_getHardwareSampleRate = load_I_PS("AAudioStream_getHardwareSampleRate");
+ stream_getHardwareFormat = load_F_PS("AAudioStream_getHardwareFormat");
}
+
return 0;
}
@@ -215,10 +246,10 @@ AAudioLoader::signature_F_PS AAudioLoader::load_F_PS(const char *functionName) {
return reinterpret_cast<signature_F_PS>(proc);
}
-AAudioLoader::signature_B_PS AAudioLoader::load_B_PS(const char *functionName) {
+AAudioLoader::signature_O_PS AAudioLoader::load_O_PS(const char *functionName) {
void *proc = dlsym(mLibHandle, functionName);
AAudioLoader_check(proc, functionName);
- return reinterpret_cast<signature_B_PS>(proc);
+ return reinterpret_cast<signature_O_PS>(proc);
}
AAudioLoader::signature_I_PB AAudioLoader::load_I_PB(const char *functionName) {
@@ -269,6 +300,12 @@ AAudioLoader::signature_U_PS AAudioLoader::load_U_PS(const char *functionName) {
return reinterpret_cast<signature_U_PS>(proc);
}
+AAudioLoader::signature_V_PBO AAudioLoader::load_V_PBO(const char *functionName) {
+ void *proc = dlsym(mLibHandle, functionName);
+ AAudioLoader_check(proc, functionName);
+ return reinterpret_cast<signature_V_PBO>(proc);
+}
+
// Ensure that all AAudio primitive data types are int32_t
#define ASSERT_INT32(type) static_assert(std::is_same<int32_t, type>::value, \
#type" must be int32_t")
@@ -385,7 +422,20 @@ AAudioLoader::signature_U_PS AAudioLoader::load_U_PS(const char *functionName) {
#endif // __NDK_MAJOR__ >= 17
-// The aaudio channel masks were added in NDK 24,
+// aaudio_allowed_capture_policy_t was added in NDK 20,
+// which is the first version to support Android Q (API 29).
+#if __NDK_MAJOR__ >= 20
+
+ ASSERT_INT32(aaudio_allowed_capture_policy_t);
+
+ static_assert((int32_t)AllowedCapturePolicy::Unspecified == AAUDIO_UNSPECIFIED, ERRMSG);
+ static_assert((int32_t)AllowedCapturePolicy::All == AAUDIO_ALLOW_CAPTURE_BY_ALL, ERRMSG);
+ static_assert((int32_t)AllowedCapturePolicy::System == AAUDIO_ALLOW_CAPTURE_BY_SYSTEM, ERRMSG);
+ static_assert((int32_t)AllowedCapturePolicy::None == AAUDIO_ALLOW_CAPTURE_BY_NONE, ERRMSG);
+
+#endif // __NDK_MAJOR__ >= 20
+
+// The aaudio channel masks and spatialization behavior were added in NDK 24,
// which is the first version to support Android SC_V2 (API 32).
#if __NDK_MAJOR__ >= 24
@@ -443,6 +493,12 @@ AAudioLoader::signature_U_PS AAudioLoader::load_U_PS(const char *functionName) {
static_assert((uint32_t)ChannelMask::CM9Point1Point6 == AAUDIO_CHANNEL_9POINT1POINT6, ERRMSG);
static_assert((uint32_t)ChannelMask::FrontBack == AAUDIO_CHANNEL_FRONT_BACK, ERRMSG);
+ ASSERT_INT32(aaudio_spatialization_behavior_t);
+
+ static_assert((int32_t)SpatializationBehavior::Unspecified == AAUDIO_UNSPECIFIED, ERRMSG);
+ static_assert((int32_t)SpatializationBehavior::Auto == AAUDIO_SPATIALIZATION_BEHAVIOR_AUTO, ERRMSG);
+ static_assert((int32_t)SpatializationBehavior::Never == AAUDIO_SPATIALIZATION_BEHAVIOR_NEVER, ERRMSG);
+
#endif
#endif // AAUDIO_AAUDIO_H
diff --git a/src/aaudio/AAudioLoader.h b/src/aaudio/AAudioLoader.h
index ea00a47f..2378464e 100644
--- a/src/aaudio/AAudioLoader.h
+++ b/src/aaudio/AAudioLoader.h
@@ -69,6 +69,15 @@ typedef int32_t aaudio_session_id_t;
#if __NDK_MAJOR__ < 24
// Defined in SC_V2
typedef uint32_t aaudio_channel_mask_t;
+typedef int32_t aaudio_spatialization_behavior_t;
+#endif
+
+#ifndef __ANDROID_API_Q__
+#define __ANDROID_API_Q__ 29
+#endif
+
+#ifndef __ANDROID_API_R__
+#define __ANDROID_API_R__ 30
#endif
#ifndef __ANDROID_API_S__
@@ -79,6 +88,10 @@ typedef uint32_t aaudio_channel_mask_t;
#define __ANDROID_API_S_V2__ 32
#endif
+#ifndef __ANDROID_API_U__
+#define __ANDROID_API_U__ 34
+#endif
+
namespace oboe {
/**
@@ -100,6 +113,8 @@ class AAudioLoader {
// C = Const prefix
// H = cHar
// U = uint32_t
+ // O = bOol
+
typedef int32_t (*signature_I_PPB)(AAudioStreamBuilder **builder);
typedef const char * (*signature_CPH_I)(int32_t);
@@ -116,6 +131,9 @@ class AAudioLoader {
typedef void (*signature_V_PBCPH)(AAudioStreamBuilder *, const char *);
+ // AAudioStreamBuilder_setPrivacySensitive
+ typedef void (*signature_V_PBO)(AAudioStreamBuilder *, bool);
+
typedef int32_t (*signature_I_PS)(AAudioStream *); // AAudioStream_getSampleRate()
typedef int64_t (*signature_L_PS)(AAudioStream *); // AAudioStream_getFramesRead()
// AAudioStream_setBufferSizeInFrames()
@@ -141,7 +159,7 @@ class AAudioLoader {
typedef int32_t (*signature_I_PSKPLPL)(AAudioStream *, clockid_t, int64_t *, int64_t *);
- typedef bool (*signature_B_PS)(AAudioStream *);
+ typedef bool (*signature_O_PS)(AAudioStream *);
typedef uint32_t (*signature_U_PS)(AAudioStream *);
@@ -181,9 +199,15 @@ class AAudioLoader {
signature_V_PBI builder_setInputPreset = nullptr;
signature_V_PBI builder_setSessionId = nullptr;
+ signature_V_PBO builder_setPrivacySensitive = nullptr;
+ signature_V_PBI builder_setAllowedCapturePolicy = nullptr;
+
signature_V_PBCPH builder_setPackageName = nullptr;
signature_V_PBCPH builder_setAttributionTag = nullptr;
+ signature_V_PBO builder_setIsContentSpatialized = nullptr;
+ signature_V_PBI builder_setSpatializationBehavior = nullptr;
+
signature_V_PBPDPV builder_setDataCallback = nullptr;
signature_V_PBPEPV builder_setErrorCallback = nullptr;
@@ -198,6 +222,7 @@ class AAudioLoader {
signature_I_PSKPLPL stream_getTimestamp = nullptr;
+ signature_I_PS stream_release = nullptr;
signature_I_PS stream_close = nullptr;
signature_I_PS stream_getChannelCount = nullptr;
@@ -228,8 +253,18 @@ class AAudioLoader {
signature_I_PS stream_getInputPreset = nullptr;
signature_I_PS stream_getSessionId = nullptr;
+ signature_O_PS stream_isPrivacySensitive = nullptr;
+ signature_I_PS stream_getAllowedCapturePolicy = nullptr;
+
signature_U_PS stream_getChannelMask = nullptr;
+ signature_O_PS stream_isContentSpatialized = nullptr;
+ signature_I_PS stream_getSpatializationBehavior = nullptr;
+
+ signature_I_PS stream_getHardwareChannelCount = nullptr;
+ signature_I_PS stream_getHardwareSampleRate = nullptr;
+ signature_F_PS stream_getHardwareFormat = nullptr;
+
private:
AAudioLoader() {}
~AAudioLoader();
@@ -246,7 +281,7 @@ class AAudioLoader {
signature_I_PS load_I_PS(const char *name);
signature_L_PS load_L_PS(const char *name);
signature_F_PS load_F_PS(const char *name);
- signature_B_PS load_B_PS(const char *name);
+ signature_O_PS load_O_PS(const char *name);
signature_I_PSI load_I_PSI(const char *name);
signature_I_PSPVIL load_I_PSPVIL(const char *name);
signature_I_PSCPVIL load_I_PSCPVIL(const char *name);
@@ -254,6 +289,7 @@ class AAudioLoader {
signature_I_PSKPLPL load_I_PSKPLPL(const char *name);
signature_V_PBU load_V_PBU(const char *name);
signature_U_PS load_U_PS(const char *name);
+ signature_V_PBO load_V_PBO(const char *name);
void *mLibHandle = nullptr;
};
diff --git a/src/aaudio/AudioStreamAAudio.cpp b/src/aaudio/AudioStreamAAudio.cpp
index abc86944..db7e658b 100644
--- a/src/aaudio/AudioStreamAAudio.cpp
+++ b/src/aaudio/AudioStreamAAudio.cpp
@@ -146,6 +146,39 @@ void AudioStreamAAudio::internalErrorCallback(
}
}
+void AudioStreamAAudio::beginPerformanceHintInCallback() {
+ if (isPerformanceHintEnabled()) {
+ if (!mAdpfOpenAttempted) {
+ int64_t targetDurationNanos = (mFramesPerBurst * 1e9) / getSampleRate();
+ // This has to be called from the callback thread so we get the right TID.
+ int adpfResult = mAdpfWrapper.open(gettid(), targetDurationNanos);
+ if (adpfResult < 0) {
+ LOGW("WARNING ADPF not supported, %d\n", adpfResult);
+ } else {
+ LOGD("ADPF is now active\n");
+ }
+ mAdpfOpenAttempted = true;
+ }
+ mAdpfWrapper.onBeginCallback();
+ } else if (!isPerformanceHintEnabled() && mAdpfOpenAttempted) {
+ LOGD("ADPF closed\n");
+ mAdpfWrapper.close();
+ mAdpfOpenAttempted = false;
+ }
+}
+
+void AudioStreamAAudio::endPerformanceHintInCallback(int32_t numFrames) {
+ if (mAdpfWrapper.isOpen()) {
+ // Scale the measured duration based on numFrames so it is normalized to a full burst.
+ double durationScaler = static_cast<double>(mFramesPerBurst) / numFrames;
+ // Skip this callback if numFrames is very small.
+ // This can happen when buffers wrap around, particularly when doing sample rate conversion.
+ if (durationScaler < 2.0) {
+ mAdpfWrapper.onEndCallback(durationScaler);
+ }
+ }
+}
+
void AudioStreamAAudio::logUnsupportedAttributes() {
int sdkVersion = getSdkVersion();
@@ -205,6 +238,20 @@ Result AudioStreamAAudio::open() {
}
mLibLoader->builder_setBufferCapacityInFrames(aaudioBuilder, capacity);
+ if (mLibLoader->builder_setSessionId != nullptr) {
+ mLibLoader->builder_setSessionId(aaudioBuilder,
+ static_cast<aaudio_session_id_t>(mSessionId));
+ // Output effects do not support PerformanceMode::LowLatency.
+ if (OboeGlobals::areWorkaroundsEnabled()
+ && mSessionId != SessionId::None
+ && mDirection == oboe::Direction::Output
+ && mPerformanceMode == PerformanceMode::LowLatency) {
+ mPerformanceMode = PerformanceMode::None;
+ LOGD("AudioStreamAAudio.open() performance mode changed to None when session "
+ "id is requested");
+ }
+ }
+
// Channel mask was added in SC_V2. Given the corresponding channel count of selected channel
// mask may be different from selected channel count, the last set value will be respected.
// If channel count is set after channel mask, the previously set channel mask will be cleared.
@@ -247,11 +294,6 @@ Result AudioStreamAAudio::open() {
static_cast<aaudio_input_preset_t>(inputPreset));
}
- if (mLibLoader->builder_setSessionId != nullptr) {
- mLibLoader->builder_setSessionId(aaudioBuilder,
- static_cast<aaudio_session_id_t>(mSessionId));
- }
-
// These were added in S so we have to check for the function pointer.
if (mLibLoader->builder_setPackageName != nullptr && !mPackageName.empty()) {
mLibLoader->builder_setPackageName(aaudioBuilder,
@@ -263,6 +305,33 @@ Result AudioStreamAAudio::open() {
mAttributionTag.c_str());
}
+ // This was added in Q so we have to check for the function pointer.
+ if (mLibLoader->builder_setAllowedCapturePolicy != nullptr && mDirection == oboe::Direction::Output) {
+ mLibLoader->builder_setAllowedCapturePolicy(aaudioBuilder,
+ static_cast<aaudio_allowed_capture_policy_t>(mAllowedCapturePolicy));
+ }
+
+ if (mLibLoader->builder_setPrivacySensitive != nullptr && mDirection == oboe::Direction::Input
+ && mPrivacySensitiveMode != PrivacySensitiveMode::Unspecified) {
+ mLibLoader->builder_setPrivacySensitive(aaudioBuilder,
+ mPrivacySensitiveMode == PrivacySensitiveMode::Enabled);
+ }
+
+ if (mLibLoader->builder_setIsContentSpatialized != nullptr) {
+ mLibLoader->builder_setIsContentSpatialized(aaudioBuilder, mIsContentSpatialized);
+ }
+
+ if (mLibLoader->builder_setSpatializationBehavior != nullptr) {
+ // Override Unspecified as Never to reduce latency.
+ if (mSpatializationBehavior == SpatializationBehavior::Unspecified) {
+ mSpatializationBehavior = SpatializationBehavior::Never;
+ }
+ mLibLoader->builder_setSpatializationBehavior(aaudioBuilder,
+ static_cast<aaudio_spatialization_behavior_t>(mSpatializationBehavior));
+ } else {
+ mSpatializationBehavior = SpatializationBehavior::Never;
+ }
+
if (isDataCallbackSpecified()) {
mLibLoader->builder_setDataCallback(aaudioBuilder, oboe_aaudio_data_callback_proc, this);
mLibLoader->builder_setFramesPerDataCallback(aaudioBuilder, getFramesPerDataCallback());
@@ -320,10 +389,44 @@ Result AudioStreamAAudio::open() {
mSessionId = SessionId::None;
}
+ // This was added in Q so we have to check for the function pointer.
+ if (mLibLoader->stream_getAllowedCapturePolicy != nullptr && mDirection == oboe::Direction::Output) {
+ mAllowedCapturePolicy = static_cast<AllowedCapturePolicy>(mLibLoader->stream_getAllowedCapturePolicy(mAAudioStream));
+ } else {
+ mAllowedCapturePolicy = AllowedCapturePolicy::Unspecified;
+ }
+
+ if (mLibLoader->stream_isPrivacySensitive != nullptr && mDirection == oboe::Direction::Input) {
+ bool isPrivacySensitive = mLibLoader->stream_isPrivacySensitive(mAAudioStream);
+ mPrivacySensitiveMode = isPrivacySensitive ? PrivacySensitiveMode::Enabled :
+ PrivacySensitiveMode::Disabled;
+ } else {
+ mPrivacySensitiveMode = PrivacySensitiveMode::Unspecified;
+ }
+
if (mLibLoader->stream_getChannelMask != nullptr) {
mChannelMask = static_cast<ChannelMask>(mLibLoader->stream_getChannelMask(mAAudioStream));
}
+ if (mLibLoader->stream_isContentSpatialized != nullptr) {
+ mIsContentSpatialized = mLibLoader->stream_isContentSpatialized(mAAudioStream);
+ }
+
+ if (mLibLoader->stream_getSpatializationBehavior != nullptr) {
+ mSpatializationBehavior = static_cast<SpatializationBehavior>(
+ mLibLoader->stream_getSpatializationBehavior(mAAudioStream));
+ }
+
+ if (mLibLoader->stream_getHardwareChannelCount != nullptr) {
+ mHardwareChannelCount = mLibLoader->stream_getHardwareChannelCount(mAAudioStream);
+ }
+ if (mLibLoader->stream_getHardwareSampleRate != nullptr) {
+ mHardwareSampleRate = mLibLoader->stream_getHardwareSampleRate(mAAudioStream);
+ }
+ if (mLibLoader->stream_getHardwareFormat != nullptr) {
+ mHardwareFormat = static_cast<AudioFormat>(mLibLoader->stream_getHardwareFormat(mAAudioStream));
+ }
+
LOGD("AudioStreamAAudio.open() format=%d, sampleRate=%d, capacity = %d",
static_cast<int>(mFormat), static_cast<int>(mSampleRate),
static_cast<int>(mBufferCapacityInFrames));
@@ -332,11 +435,46 @@ Result AudioStreamAAudio::open() {
error2:
mLibLoader->builder_delete(aaudioBuilder);
- LOGD("AudioStreamAAudio.open: AAudioStream_Open() returned %s",
- mLibLoader->convertResultToText(static_cast<aaudio_result_t>(result)));
+ if (static_cast<int>(result) > 0) {
+ // Possibly due to b/267531411
+ LOGW("AudioStreamAAudio.open: AAudioStream_Open() returned positive error = %d",
+ static_cast<int>(result));
+ if (OboeGlobals::areWorkaroundsEnabled()) {
+ result = Result::ErrorInternal; // Coerce to negative error.
+ }
+ } else {
+ LOGD("AudioStreamAAudio.open: AAudioStream_Open() returned %s = %d",
+ mLibLoader->convertResultToText(static_cast<aaudio_result_t>(result)),
+ static_cast<int>(result));
+ }
return result;
}
+Result AudioStreamAAudio::release() {
+ if (getSdkVersion() < __ANDROID_API_R__) {
+ return Result::ErrorUnimplemented;
+ }
+
+ // AAudioStream_release() is buggy on Android R.
+ if (OboeGlobals::areWorkaroundsEnabled() && getSdkVersion() == __ANDROID_API_R__) {
+ LOGW("Skipping release() on Android R");
+ return Result::ErrorUnimplemented;
+ }
+
+ std::lock_guard<std::mutex> lock(mLock);
+ AAudioStream *stream = mAAudioStream.load();
+ if (stream != nullptr) {
+ if (OboeGlobals::areWorkaroundsEnabled()) {
+ // Make sure we are really stopped. Do it under mLock
+ // so another thread cannot call requestStart() right before the close.
+ requestStop_l(stream);
+ }
+ return static_cast<Result>(mLibLoader->stream_release(stream));
+ } else {
+ return Result::ErrorClosed;
+ }
+}
+
Result AudioStreamAAudio::close() {
// Prevent two threads from closing the stream at the same time and crashing.
// This could occur, for example, if an application called close() at the same
@@ -419,6 +557,7 @@ Result AudioStreamAAudio::requestStart() {
setDataCallbackEnabled(true);
}
mStopThreadAllowed = true;
+ closePerformanceHint();
return static_cast<Result>(mLibLoader->stream_requestStart(stream));
} else {
return Result::ErrorClosed;
diff --git a/src/aaudio/AudioStreamAAudio.h b/src/aaudio/AudioStreamAAudio.h
index 9846423f..2df4d857 100644
--- a/src/aaudio/AudioStreamAAudio.h
+++ b/src/aaudio/AudioStreamAAudio.h
@@ -22,6 +22,7 @@
#include <mutex>
#include <thread>
+#include <common/AdpfWrapper.h>
#include "oboe/AudioStreamBuilder.h"
#include "oboe/AudioStream.h"
#include "oboe/Definitions.h"
@@ -51,6 +52,7 @@ public:
// These functions override methods in AudioStream.
// See AudioStream for documentation.
Result open() override;
+ Result release() override;
Result close() override;
Result requestStart() override;
@@ -93,6 +95,11 @@ public:
bool isMMapUsed();
+ void closePerformanceHint() override {
+ mAdpfWrapper.close();
+ mAdpfOpenAttempted = false;
+ }
+
protected:
static void internalErrorCallback(
AAudioStream *stream,
@@ -108,6 +115,14 @@ protected:
void logUnsupportedAttributes();
+ void beginPerformanceHintInCallback() override;
+
+ void endPerformanceHintInCallback(int32_t numFrames) override;
+
+ // set by callback (or app when idle)
+ std::atomic<bool> mAdpfOpenAttempted{false};
+ AdpfWrapper mAdpfWrapper;
+
private:
// Must call under mLock. And stream must NOT be nullptr.
Result requestStop_l(AAudioStream *stream);
@@ -117,11 +132,6 @@ private:
*/
void launchStopThread();
-public:
- int32_t getMDelayBeforeCloseMillis() const;
-
- void setDelayBeforeCloseMillis(int32_t mDelayBeforeCloseMillis);
-
private:
std::atomic<bool> mCallbackThreadEnabled;
diff --git a/src/common/AdpfWrapper.cpp b/src/common/AdpfWrapper.cpp
new file mode 100644
index 00000000..05accdea
--- /dev/null
+++ b/src/common/AdpfWrapper.cpp
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2021 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 <dlfcn.h>
+#include <stdint.h>
+#include <sys/types.h>
+
+#include "AdpfWrapper.h"
+#include "AudioClock.h"
+#include "OboeDebug.h"
+
+typedef APerformanceHintManager* (*APH_getManager)();
+typedef APerformanceHintSession* (*APH_createSession)(APerformanceHintManager*, const int32_t*,
+ size_t, int64_t);
+typedef void (*APH_reportActualWorkDuration)(APerformanceHintSession*, int64_t);
+typedef void (*APH_closeSession)(APerformanceHintSession* session);
+
+static bool gAPerformanceHintBindingInitialized = false;
+static APH_getManager gAPH_getManagerFn = nullptr;
+static APH_createSession gAPH_createSessionFn = nullptr;
+static APH_reportActualWorkDuration gAPH_reportActualWorkDurationFn = nullptr;
+static APH_closeSession gAPH_closeSessionFn = nullptr;
+
+static int loadAphFunctions() {
+ if (gAPerformanceHintBindingInitialized) return true;
+
+ void* handle_ = dlopen("libandroid.so", RTLD_NOW | RTLD_NODELETE);
+ if (handle_ == nullptr) {
+ return -1000;
+ }
+
+ gAPH_getManagerFn = (APH_getManager)dlsym(handle_, "APerformanceHint_getManager");
+ if (gAPH_getManagerFn == nullptr) {
+ return -1001;
+ }
+
+ gAPH_createSessionFn = (APH_createSession)dlsym(handle_, "APerformanceHint_createSession");
+ if (gAPH_getManagerFn == nullptr) {
+ return -1002;
+ }
+
+ gAPH_reportActualWorkDurationFn = (APH_reportActualWorkDuration)dlsym(
+ handle_, "APerformanceHint_reportActualWorkDuration");
+ if (gAPH_getManagerFn == nullptr) {
+ return -1003;
+ }
+
+ gAPH_closeSessionFn = (APH_closeSession)dlsym(handle_, "APerformanceHint_closeSession");
+ if (gAPH_getManagerFn == nullptr) {
+ return -1004;
+ }
+
+ gAPerformanceHintBindingInitialized = true;
+ return 0;
+}
+
+bool AdpfWrapper::sUseAlternativeHack = false; // TODO remove hack
+
+int AdpfWrapper::open(pid_t threadId,
+ int64_t targetDurationNanos) {
+ std::lock_guard<std::mutex> lock(mLock);
+ int result = loadAphFunctions();
+ if (result < 0) return result;
+
+ // This is a singleton.
+ APerformanceHintManager* manager = gAPH_getManagerFn();
+
+ int32_t thread32 = threadId;
+ if (sUseAlternativeHack) {
+ // TODO Remove this hack when we finish experimenting with alternative algorithms.
+ // The A5 is an arbitrary signal to a hacked version of ADPF to try an alternative
+ // algorithm that is not based on PID.
+ targetDurationNanos = (targetDurationNanos & ~0xFF) | 0xA5;
+ }
+ mHintSession = gAPH_createSessionFn(manager, &thread32, 1 /* size */, targetDurationNanos);
+ if (mHintSession == nullptr) {
+ return -1;
+ }
+ return 0;
+}
+
+void AdpfWrapper::reportActualDuration(int64_t actualDurationNanos) {
+ //LOGD("ADPF Oboe %s(dur=%lld)", __func__, (long long)actualDurationNanos);
+ std::lock_guard<std::mutex> lock(mLock);
+ if (mHintSession != nullptr) {
+ gAPH_reportActualWorkDurationFn(mHintSession, actualDurationNanos);
+ }
+}
+
+void AdpfWrapper::close() {
+ std::lock_guard<std::mutex> lock(mLock);
+ if (mHintSession != nullptr) {
+ gAPH_closeSessionFn(mHintSession);
+ mHintSession = nullptr;
+ }
+}
+
+void AdpfWrapper::onBeginCallback() {
+ if (isOpen()) {
+ mBeginCallbackNanos = oboe::AudioClock::getNanoseconds(CLOCK_REALTIME);
+ }
+}
+
+void AdpfWrapper::onEndCallback(double durationScaler) {
+ if (isOpen()) {
+ int64_t endCallbackNanos = oboe::AudioClock::getNanoseconds(CLOCK_REALTIME);
+ int64_t actualDurationNanos = endCallbackNanos - mBeginCallbackNanos;
+ int64_t scaledDurationNanos = static_cast<int64_t>(actualDurationNanos * durationScaler);
+ reportActualDuration(scaledDurationNanos);
+ }
+}
diff --git a/src/common/AdpfWrapper.h b/src/common/AdpfWrapper.h
new file mode 100644
index 00000000..330ee3c6
--- /dev/null
+++ b/src/common/AdpfWrapper.h
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2021 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 SYNTHMARK_ADPF_WRAPPER_H
+#define SYNTHMARK_ADPF_WRAPPER_H
+
+#include <algorithm>
+#include <functional>
+#include <stdint.h>
+#include <sys/types.h>
+#include <unistd.h>
+#include <mutex>
+
+struct APerformanceHintManager;
+struct APerformanceHintSession;
+
+typedef struct APerformanceHintManager APerformanceHintManager;
+typedef struct APerformanceHintSession APerformanceHintSession;
+
+class AdpfWrapper {
+public:
+ /**
+ * Create an ADPF session that can be used to boost performance.
+ * @param threadId
+ * @param targetDurationNanos - nominal period of isochronous task
+ * @return zero or negative error
+ */
+ int open(pid_t threadId,
+ int64_t targetDurationNanos);
+
+ bool isOpen() const {
+ return (mHintSession != nullptr);
+ }
+
+ void close();
+
+ /**
+ * Call this at the beginning of the callback that you are measuring.
+ */
+ void onBeginCallback();
+
+ /**
+ * Call this at the end of the callback that you are measuring.
+ * It is OK to skip this if you have a short callback.
+ */
+ void onEndCallback(double durationScaler);
+
+ /**
+ * For internal use only!
+ * This is a hack for communicating with experimental versions of ADPF.
+ * @param enabled
+ */
+ static void setUseAlternative(bool enabled) {
+ sUseAlternativeHack = enabled;
+ }
+
+ /**
+ * Report the measured duration of a callback.
+ * This is normally called by onEndCallback().
+ * You may want to call this directly in order to give an advance hint of a jump in workload.
+ * @param actualDurationNanos
+ */
+ void reportActualDuration(int64_t actualDurationNanos);
+
+private:
+ std::mutex mLock;
+ APerformanceHintSession* mHintSession = nullptr;
+ int64_t mBeginCallbackNanos = 0;
+ static bool sUseAlternativeHack;
+};
+
+#endif //SYNTHMARK_ADPF_WRAPPER_H
diff --git a/src/common/AudioStream.cpp b/src/common/AudioStream.cpp
index 7f9ed4d5..06c01118 100644
--- a/src/common/AudioStream.cpp
+++ b/src/common/AudioStream.cpp
@@ -33,6 +33,7 @@ AudioStream::AudioStream(const AudioStreamBuilder &builder)
}
Result AudioStream::close() {
+ closePerformanceHint();
// Update local counters so they can be read after the close.
updateFramesWritten();
updateFramesRead();
@@ -58,6 +59,9 @@ DataCallbackResult AudioStream::fireDataCallback(void *audioData, int32_t numFra
return DataCallbackResult::Stop; // Should not be getting called
}
+ beginPerformanceHintInCallback();
+
+ // Call the app to do the work.
DataCallbackResult result;
if (mDataCallback) {
result = mDataCallback->onAudioReady(this, audioData, numFrames);
@@ -68,6 +72,8 @@ DataCallbackResult AudioStream::fireDataCallback(void *audioData, int32_t numFra
// So block that here.
setDataCallbackEnabled(result == DataCallbackResult::Continue);
+ endPerformanceHintInCallback(numFrames);
+
return result;
}
diff --git a/src/common/AudioStreamBuilder.cpp b/src/common/AudioStreamBuilder.cpp
index 5dbe38cc..f655f9fc 100644
--- a/src/common/AudioStreamBuilder.cpp
+++ b/src/common/AudioStreamBuilder.cpp
@@ -89,6 +89,11 @@ bool AudioStreamBuilder::isCompatible(AudioStreamBase &other) {
}
Result AudioStreamBuilder::openStream(AudioStream **streamPP) {
+ LOGW("Passing AudioStream pointer deprecated, Use openStream(std::shared_ptr<oboe::AudioStream> &stream) instead.");
+ return openStreamInternal(streamPP);
+}
+
+Result AudioStreamBuilder::openStreamInternal(AudioStream **streamPP) {
auto result = isValidConfig();
if (result != Result::OK) {
LOGW("%s() invalid config %d", __func__, result);
@@ -202,6 +207,7 @@ Result AudioStreamBuilder::openStream(AudioStream **streamPP) {
}
Result AudioStreamBuilder::openManagedStream(oboe::ManagedStream &stream) {
+ LOGW("`openManagedStream` is deprecated. Use openStream(std::shared_ptr<oboe::AudioStream> &stream) instead.");
stream.reset();
AudioStream *streamptr;
auto result = openStream(&streamptr);
@@ -212,7 +218,7 @@ Result AudioStreamBuilder::openManagedStream(oboe::ManagedStream &stream) {
Result AudioStreamBuilder::openStream(std::shared_ptr<AudioStream> &sharedStream) {
sharedStream.reset();
AudioStream *streamptr;
- auto result = openStream(&streamptr);
+ auto result = openStreamInternal(&streamptr);
if (result == Result::OK) {
sharedStream.reset(streamptr);
// Save a weak_ptr in the stream for use with callbacks.
diff --git a/src/common/FilterAudioStream.h b/src/common/FilterAudioStream.h
index 2f50e3ec..18907499 100644
--- a/src/common/FilterAudioStream.h
+++ b/src/common/FilterAudioStream.h
@@ -55,9 +55,13 @@ public:
// Copy parameters that may not match builder.
mBufferCapacityInFrames = mChildStream->getBufferCapacityInFrames();
mPerformanceMode = mChildStream->getPerformanceMode();
+ mSharingMode = mChildStream->getSharingMode();
mInputPreset = mChildStream->getInputPreset();
mFramesPerBurst = mChildStream->getFramesPerBurst();
mDeviceId = mChildStream->getDeviceId();
+ mHardwareSampleRate = mChildStream->getHardwareSampleRate();
+ mHardwareChannelCount = mChildStream->getHardwareChannelCount();
+ mHardwareFormat = mChildStream->getHardwareFormat();
}
virtual ~FilterAudioStream() = default;
diff --git a/src/common/QuirksManager.cpp b/src/common/QuirksManager.cpp
index 16425fb0..f9890be5 100644
--- a/src/common/QuirksManager.cpp
+++ b/src/common/QuirksManager.cpp
@@ -200,6 +200,13 @@ bool QuirksManager::isConversionNeeded(
const bool isLowLatency = builder.getPerformanceMode() == PerformanceMode::LowLatency;
const bool isInput = builder.getDirection() == Direction::Input;
const bool isFloat = builder.getFormat() == AudioFormat::Float;
+ const bool isIEC61937 = builder.getFormat() == AudioFormat::IEC61937;
+
+ // There should be no conversion for IEC61937. Sample rates and channel counts must be set explicitly.
+ if (isIEC61937) {
+ LOGI("QuirksManager::%s() conversion not needed for IEC61937", __func__);
+ return false;
+ }
// There are multiple bugs involving using callback with a specified callback size.
// Issue #778: O to Q had a problem with Legacy INPUT streams for FLOAT streams
diff --git a/src/common/Utilities.cpp b/src/common/Utilities.cpp
index 22296e68..f6718afc 100644
--- a/src/common/Utilities.cpp
+++ b/src/common/Utilities.cpp
@@ -66,6 +66,9 @@ int32_t convertFormatToSizeInBytes(AudioFormat format) {
case AudioFormat::I32:
size = sizeof(int32_t);
break;
+ case AudioFormat::IEC61937:
+ size = sizeof(int16_t);
+ break;
default:
break;
}
@@ -106,6 +109,7 @@ const char *convertToText<AudioFormat>(AudioFormat format) {
case AudioFormat::Float: return "Float";
case AudioFormat::I24: return "I24";
case AudioFormat::I32: return "I32";
+ case AudioFormat::IEC61937: return "IEC61937";
default: return "Unrecognized format";
}
}
@@ -310,6 +314,18 @@ int getSdkVersion() {
return sCachedSdkVersion;
}
+bool isAtLeastPreReleaseCodename(const std::string& codename) {
+ std::string buildCodename = getPropertyString("ro.build.version.codename");
+ // Special case "REL", which means the build is not a pre-release build.
+ if ("REL" == buildCodename) {
+ return false;
+ }
+
+ // Otherwise lexically compare them. Return true if the build codename is equal to or
+ // greater than the requested codename.
+ return buildCodename.compare(codename) >= 0;
+}
+
int getChannelCountFromChannelMask(ChannelMask channelMask) {
return __builtin_popcount(static_cast<uint32_t>(channelMask));
}
diff --git a/src/flowgraph/FlowgraphUtilities.h b/src/flowgraph/FlowgraphUtilities.h
index 5e90588c..e277d6e6 100644
--- a/src/flowgraph/FlowgraphUtilities.h
+++ b/src/flowgraph/FlowgraphUtilities.h
@@ -17,6 +17,7 @@
#ifndef FLOWGRAPH_UTILITIES_H
#define FLOWGRAPH_UTILITIES_H
+#include <math.h>
#include <unistd.h>
using namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph;
@@ -50,6 +51,20 @@ static int32_t clamp32FromFloat(float f)
return f > 0 ? f + 0.5 : f - 0.5;
}
+/**
+ * Convert a single-precision floating point value to a Q0.23 integer value, stored in a
+ * 32 bit signed integer (technically stored as Q8.23, but clamped to Q0.23).
+ *
+ * Values outside the range [-1.0, 1.0) are properly clamped to -8388608 and 8388607,
+ * including -Inf and +Inf. NaN values are considered undefined, and behavior may change
+ * depending on hardware and future implementation of this function.
+ */
+static int32_t clamp24FromFloat(float f)
+{
+ static const float scale = 1 << 23;
+ return (int32_t) lroundf(fmaxf(fminf(f * scale, scale - 1.f), -scale));
+}
+
};
#endif // FLOWGRAPH_UTILITIES_H
diff --git a/src/flowgraph/SinkI8_24.cpp b/src/flowgraph/SinkI8_24.cpp
new file mode 100644
index 00000000..d5e4b808
--- /dev/null
+++ b/src/flowgraph/SinkI8_24.cpp
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "FlowGraphNode.h"
+#include "FlowgraphUtilities.h"
+#include "SinkI8_24.h"
+
+#if FLOWGRAPH_ANDROID_INTERNAL
+#include <audio_utils/primitives.h>
+#endif
+
+using namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph;
+
+SinkI8_24::SinkI8_24(int32_t channelCount)
+ : FlowGraphSink(channelCount) {}
+
+int32_t SinkI8_24::read(void *data, int32_t numFrames) {
+ int32_t *intData = (int32_t *) data;
+ const int32_t channelCount = input.getSamplesPerFrame();
+
+ int32_t framesLeft = numFrames;
+ while (framesLeft > 0) {
+ // Run the graph and pull data through the input port.
+ int32_t framesRead = pullData(framesLeft);
+ if (framesRead <= 0) {
+ break;
+ }
+ const float *signal = input.getBuffer();
+ int32_t numSamples = framesRead * channelCount;
+#if FLOWGRAPH_ANDROID_INTERNAL
+ memcpy_to_q8_23_from_float_with_clamp(intData, signal, numSamples);
+ intData += numSamples;
+ signal += numSamples;
+#else
+ for (int i = 0; i < numSamples; i++) {
+ *intData++ = FlowgraphUtilities::clamp24FromFloat(*signal++);
+ }
+#endif
+ framesLeft -= framesRead;
+ }
+ return numFrames - framesLeft;
+}
diff --git a/src/flowgraph/SinkI8_24.h b/src/flowgraph/SinkI8_24.h
new file mode 100644
index 00000000..366b4ba8
--- /dev/null
+++ b/src/flowgraph/SinkI8_24.h
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef FLOWGRAPH_SINK_I8_24_H
+#define FLOWGRAPH_SINK_I8_24_H
+
+#include <stdint.h>
+
+#include "FlowGraphNode.h"
+
+namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph {
+
+ class SinkI8_24 : public FlowGraphSink {
+ public:
+ explicit SinkI8_24(int32_t channelCount);
+ ~SinkI8_24() override = default;
+
+ int32_t read(void *data, int32_t numFrames) override;
+
+ const char *getName() override {
+ return "SinkI8_24";
+ }
+ };
+
+} /* namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph */
+
+#endif //FLOWGRAPH_SINK_I8_24_H
diff --git a/src/flowgraph/SourceI8_24.cpp b/src/flowgraph/SourceI8_24.cpp
new file mode 100644
index 00000000..684446cc
--- /dev/null
+++ b/src/flowgraph/SourceI8_24.cpp
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <algorithm>
+#include <unistd.h>
+
+#include "FlowGraphNode.h"
+#include "SourceI8_24.h"
+
+#if FLOWGRAPH_ANDROID_INTERNAL
+#include <audio_utils/primitives.h>
+#endif
+
+using namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph;
+
+SourceI8_24::SourceI8_24(int32_t channelCount)
+ : FlowGraphSourceBuffered(channelCount) {
+}
+
+int32_t SourceI8_24::onProcess(int32_t numFrames) {
+ float *floatData = output.getBuffer();
+ const int32_t channelCount = output.getSamplesPerFrame();
+
+ const int32_t framesLeft = mSizeInFrames - mFrameIndex;
+ const int32_t framesToProcess = std::min(numFrames, framesLeft);
+ const int32_t numSamples = framesToProcess * channelCount;
+
+ const int32_t *intBase = static_cast<const int32_t *>(mData);
+ const int32_t *intData = &intBase[mFrameIndex * channelCount];
+
+#if FLOWGRAPH_ANDROID_INTERNAL
+ memcpy_to_float_from_q8_23(floatData, intData, numSamples);
+#else
+ for (int i = 0; i < numSamples; i++) {
+ *floatData++ = *intData++ * kScale;
+ }
+#endif
+
+ mFrameIndex += framesToProcess;
+ return framesToProcess;
+}
diff --git a/src/flowgraph/SourceI8_24.h b/src/flowgraph/SourceI8_24.h
new file mode 100644
index 00000000..91c756c8
--- /dev/null
+++ b/src/flowgraph/SourceI8_24.h
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef FLOWGRAPH_SOURCE_I8_24_H
+#define FLOWGRAPH_SOURCE_I8_24_H
+
+#include <stdint.h>
+
+#include "FlowGraphNode.h"
+
+namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph {
+
+class SourceI8_24 : public FlowGraphSourceBuffered {
+public:
+ explicit SourceI8_24(int32_t channelCount);
+ ~SourceI8_24() override = default;
+
+ int32_t onProcess(int32_t numFrames) override;
+
+ const char *getName() override {
+ return "SourceI8_24";
+ }
+private:
+ static constexpr float kScale = 1.0 / (1UL << 23);
+};
+
+} /* namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph */
+
+#endif //FLOWGRAPH_SOURCE_I8_24_H
diff --git a/src/flowgraph/resampler/MultiChannelResampler.cpp b/src/flowgraph/resampler/MultiChannelResampler.cpp
index a3ce58cd..245b669d 100644
--- a/src/flowgraph/resampler/MultiChannelResampler.cpp
+++ b/src/flowgraph/resampler/MultiChannelResampler.cpp
@@ -120,7 +120,7 @@ void MultiChannelResampler::writeFrame(const float *frame) {
}
float MultiChannelResampler::sinc(float radians) {
- if (abs(radians) < 1.0e-9) return 1.0f; // avoid divide by zero
+ if (fabsf(radians) < 1.0e-9f) return 1.0f; // avoid divide by zero
return sinf(radians) / radians; // Sinc function
}
@@ -135,10 +135,9 @@ void MultiChannelResampler::generateCoefficients(int32_t inputRate,
int coefficientIndex = 0;
double phase = 0.0; // ranges from 0.0 to 1.0, fraction between samples
// Stretch the sinc function for low pass filtering.
- const float cutoffScaler = normalizedCutoff *
- ((outputRate < inputRate)
- ? ((float)outputRate / inputRate)
- : ((float)inputRate / outputRate));
+ const float cutoffScaler = (outputRate < inputRate)
+ ? (normalizedCutoff * (float)outputRate / inputRate)
+ : 1.0f; // Do not filter when upsampling.
const int numTapsHalf = getNumTaps() / 2; // numTaps must be even.
const float numTapsHalfInverse = 1.0f / numTapsHalf;
for (int i = 0; i < numRows; i++) {
diff --git a/src/flowgraph/resampler/MultiChannelResampler.h b/src/flowgraph/resampler/MultiChannelResampler.h
index 717f3fda..9e47335a 100644
--- a/src/flowgraph/resampler/MultiChannelResampler.h
+++ b/src/flowgraph/resampler/MultiChannelResampler.h
@@ -111,6 +111,9 @@ public:
* Set lower to reduce aliasing.
* Default is 0.70.
*
+ * Note that this value is ignored when upsampling, which is when
+ * the outputRate is higher than the inputRate.
+ *
* @param normalizedCutoff anti-aliasing filter cutoff
* @return address of this builder for chaining calls
*/
@@ -227,6 +230,10 @@ protected:
/**
* Generate the filter coefficients in optimal order.
+ *
+ * Note that normalizedCutoff is ignored when upsampling, which is when
+ * the outputRate is higher than the inputRate.
+ *
* @param inputRate sample rate of the input stream
* @param outputRate sample rate of the output stream
* @param numRows number of rows in the array that contain a set of tap coefficients
diff --git a/src/flowgraph/resampler/PolyphaseResampler.cpp b/src/flowgraph/resampler/PolyphaseResampler.cpp
index e47ee8e2..c16273b7 100644
--- a/src/flowgraph/resampler/PolyphaseResampler.cpp
+++ b/src/flowgraph/resampler/PolyphaseResampler.cpp
@@ -14,6 +14,7 @@
* limitations under the License.
*/
+#include <algorithm> // Do NOT delete. Needed for LLVM. See #1746
#include <cassert>
#include <math.h>
#include "IntegerRatio.h"
diff --git a/src/flowgraph/resampler/SincResampler.cpp b/src/flowgraph/resampler/SincResampler.cpp
index a14ee47e..919f328a 100644
--- a/src/flowgraph/resampler/SincResampler.cpp
+++ b/src/flowgraph/resampler/SincResampler.cpp
@@ -14,6 +14,7 @@
* limitations under the License.
*/
+#include <algorithm> // Do NOT delete. Needed for LLVM. See #1746
#include <cassert>
#include <math.h>
#include "SincResampler.h"
diff --git a/src/flowgraph/resampler/SincResamplerStereo.cpp b/src/flowgraph/resampler/SincResamplerStereo.cpp
index d459abf6..f9287baa 100644
--- a/src/flowgraph/resampler/SincResamplerStereo.cpp
+++ b/src/flowgraph/resampler/SincResamplerStereo.cpp
@@ -14,6 +14,7 @@
* limitations under the License.
*/
+#include <algorithm> // Do NOT delete. Needed for LLVM. See #1746
#include <cassert>
#include <math.h>
diff --git a/src/opensles/AudioInputStreamOpenSLES.cpp b/src/opensles/AudioInputStreamOpenSLES.cpp
index 65298c4e..3653d964 100644
--- a/src/opensles/AudioInputStreamOpenSLES.cpp
+++ b/src/opensles/AudioInputStreamOpenSLES.cpp
@@ -265,6 +265,8 @@ Result AudioInputStreamOpenSLES::requestStart() {
setState(StreamState::Starting);
+ closePerformanceHint();
+
if (getBufferDepth(mSimpleBufferQueueInterface) == 0) {
// Enqueue the first buffer to start the streaming.
// This does not call the callback function.
diff --git a/src/opensles/AudioOutputStreamOpenSLES.cpp b/src/opensles/AudioOutputStreamOpenSLES.cpp
index 6d480d2c..0ef87dd0 100644
--- a/src/opensles/AudioOutputStreamOpenSLES.cpp
+++ b/src/opensles/AudioOutputStreamOpenSLES.cpp
@@ -30,9 +30,10 @@
using namespace oboe;
static SLuint32 OpenSLES_convertOutputUsage(Usage oboeUsage) {
- SLuint32 openslStream = SL_ANDROID_STREAM_MEDIA;
+ SLuint32 openslStream;
switch(oboeUsage) {
case Usage::Media:
+ case Usage::Game:
openslStream = SL_ANDROID_STREAM_MEDIA;
break;
case Usage::VoiceCommunication:
@@ -43,18 +44,15 @@ static SLuint32 OpenSLES_convertOutputUsage(Usage oboeUsage) {
openslStream = SL_ANDROID_STREAM_ALARM;
break;
case Usage::Notification:
- case Usage::NotificationRingtone:
case Usage::NotificationEvent:
openslStream = SL_ANDROID_STREAM_NOTIFICATION;
break;
+ case Usage::NotificationRingtone:
+ openslStream = SL_ANDROID_STREAM_RING;
+ break;
case Usage::AssistanceAccessibility:
case Usage::AssistanceNavigationGuidance:
case Usage::AssistanceSonification:
- openslStream = SL_ANDROID_STREAM_SYSTEM;
- break;
- case Usage::Game:
- openslStream = SL_ANDROID_STREAM_MEDIA;
- break;
case Usage::Assistant:
default:
openslStream = SL_ANDROID_STREAM_SYSTEM;
@@ -291,6 +289,7 @@ Result AudioOutputStreamOpenSLES::requestStart() {
setDataCallbackEnabled(true);
setState(StreamState::Starting);
+ closePerformanceHint();
if (getBufferDepth(mSimpleBufferQueueInterface) == 0) {
// Enqueue the first buffer if needed to start the streaming.
diff --git a/src/opensles/AudioStreamOpenSLES.cpp b/src/opensles/AudioStreamOpenSLES.cpp
index 62692a42..9013d61c 100644
--- a/src/opensles/AudioStreamOpenSLES.cpp
+++ b/src/opensles/AudioStreamOpenSLES.cpp
@@ -108,6 +108,13 @@ Result AudioStreamOpenSLES::open() {
SLresult AudioStreamOpenSLES::finishCommonOpen(SLAndroidConfigurationItf configItf) {
+ // Setting privacy sensitive mode and allowed capture policy are not supported for OpenSL ES.
+ mPrivacySensitiveMode = PrivacySensitiveMode::Unspecified;
+ mAllowedCapturePolicy = AllowedCapturePolicy::Unspecified;
+
+ // Spatialization Behavior is not supported for OpenSL ES.
+ mSpatializationBehavior = SpatializationBehavior::Never;
+
SLresult result = registerBufferQueueCallback();
if (SL_RESULT_SUCCESS != result) {
return result;
@@ -285,6 +292,24 @@ void AudioStreamOpenSLES::logUnsupportedAttributes() {
LOGW("SessionId [AudioStreamBuilder::setSessionId()] "
"is not supported on OpenSLES streams.");
}
+
+ // Privacy Sensitive Mode
+ if (mPrivacySensitiveMode != PrivacySensitiveMode::Unspecified) {
+ LOGW("PrivacySensitiveMode [AudioStreamBuilder::setPrivacySensitiveMode()] "
+ "is not supported on OpenSLES streams.");
+ }
+
+ // Spatialization Behavior
+ if (mSpatializationBehavior != SpatializationBehavior::Unspecified) {
+ LOGW("SpatializationBehavior [AudioStreamBuilder::setSpatializationBehavior()] "
+ "is not supported on OpenSLES streams.");
+ }
+
+ // Allowed Capture Policy
+ if (mAllowedCapturePolicy != AllowedCapturePolicy::Unspecified) {
+ LOGW("AllowedCapturePolicy [AudioStreamBuilder::setAllowedCapturePolicy()] "
+ "is not supported on OpenSLES streams.");
+ }
}
SLresult AudioStreamOpenSLES::configurePerformanceMode(SLAndroidConfigurationItf configItf) {
@@ -358,6 +383,7 @@ Result AudioStreamOpenSLES::close_l() {
EngineOpenSLES::getInstance().close();
setState(StreamState::Closed);
+
return Result::OK;
}
diff --git a/src/opensles/OpenSLESUtilities.cpp b/src/opensles/OpenSLESUtilities.cpp
index 6d25e79f..534f641c 100644
--- a/src/opensles/OpenSLESUtilities.cpp
+++ b/src/opensles/OpenSLESUtilities.cpp
@@ -85,6 +85,7 @@ SLuint32 OpenSLES_ConvertFormatToRepresentation(AudioFormat format) {
return SL_ANDROID_PCM_REPRESENTATION_FLOAT;
case AudioFormat::I24:
case AudioFormat::I32:
+ case AudioFormat::IEC61937:
case AudioFormat::Invalid:
case AudioFormat::Unspecified:
default:
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index ddbff2d5..45b1f8c7 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -28,6 +28,7 @@ add_executable(
testOboe
testAAudio.cpp
testFlowgraph.cpp
+ testFullDuplexStream.cpp
testResampler.cpp
testReturnStop.cpp
testStreamClosedMethods.cpp
diff --git a/tests/README b/tests/README
deleted file mode 100644
index c78a99e3..00000000
--- a/tests/README
+++ /dev/null
@@ -1 +0,0 @@
-See run_tests.sh for documentation
diff --git a/tests/UnitTestRunner/app/build.gradle b/tests/UnitTestRunner/app/build.gradle
index 8547843f..58bf4aaa 100644
--- a/tests/UnitTestRunner/app/build.gradle
+++ b/tests/UnitTestRunner/app/build.gradle
@@ -1,11 +1,11 @@
apply plugin: 'com.android.application'
android {
- compileSdkVersion 33
+ compileSdkVersion 34
defaultConfig {
applicationId "com.google.oboe.tests.unittestrunner"
minSdkVersion 21
- targetSdkVersion 33
+ targetSdkVersion 34
versionCode 1
versionName "1.0"
}
diff --git a/tests/UnitTestRunner/app/src/main/java/com/google/oboe/tests/unittestrunner/MainActivity.java b/tests/UnitTestRunner/app/src/main/java/com/google/oboe/tests/unittestrunner/MainActivity.java
index 75414745..97c939ad 100644
--- a/tests/UnitTestRunner/app/src/main/java/com/google/oboe/tests/unittestrunner/MainActivity.java
+++ b/tests/UnitTestRunner/app/src/main/java/com/google/oboe/tests/unittestrunner/MainActivity.java
@@ -176,6 +176,7 @@ public class MainActivity extends AppCompatActivity {
.show();
} else {
// Permission was granted, run the command
+ outputText.setText(R.string.status_record_audio_granted);
runCommand();
}
}
diff --git a/tests/UnitTestRunner/app/src/main/res/values/strings.xml b/tests/UnitTestRunner/app/src/main/res/values/strings.xml
index 031b6511..e37401c2 100644
--- a/tests/UnitTestRunner/app/src/main/res/values/strings.xml
+++ b/tests/UnitTestRunner/app/src/main/res/values/strings.xml
@@ -2,5 +2,5 @@
<string name="app_name">Unit Test Runner</string>
<string name="need_record_audio_permission">"This app needs RECORD_AUDIO permission"</string>
<string name="status_record_audio_denied">Error: Permission for RECORD_AUDIO was denied</string>
- <string name="status_touch_to_begin">RECORD_AUDIO permission granted, touch START to begin</string>
+ <string name="status_record_audio_granted">RECORD_AUDIO permission granted. Running tests...</string>
</resources>
diff --git a/tests/run_tests.sh b/tests/run_tests.sh
index d20b3bc9..c2325bbc 100755
--- a/tests/run_tests.sh
+++ b/tests/run_tests.sh
@@ -92,7 +92,7 @@ rm -r ${BUILD_DIR}
# Configure the build
echo "Building tests for ${ABI} using ${PLATFORM}"
-CMAKE_ARGS="-H. \
+CMAKE_ARGS="-S. \
-B${BUILD_DIR} \
-DANDROID_ABI=${ABI} \
-DANDROID_PLATFORM=${PLATFORM} \
diff --git a/tests/testFullDuplexStream.cpp b/tests/testFullDuplexStream.cpp
new file mode 100644
index 00000000..b3b96f2a
--- /dev/null
+++ b/tests/testFullDuplexStream.cpp
@@ -0,0 +1,208 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <thread>
+
+#include <gtest/gtest.h>
+
+#include <oboe/Oboe.h>
+
+using namespace oboe;
+
+static constexpr int kTimeToSleepMicros = 5 * 1000 * 1000; // 5 s
+
+using TestFullDuplexStreamParams = std::tuple<AudioApi, PerformanceMode, AudioApi, PerformanceMode>;
+
+class TestFullDuplexStream : public ::testing::Test,
+ public ::testing::WithParamInterface<TestFullDuplexStreamParams>,
+ public FullDuplexStream {
+public:
+ DataCallbackResult onBothStreamsReady(
+ const void *inputData,
+ int numInputFrames,
+ void *outputData,
+ int numOutputFrames) override {
+ mCallbackCount++;
+ if (numInputFrames == numOutputFrames) {
+ mGoodCallbackCount++;
+ }
+ return DataCallbackResult::Continue;
+ }
+
+protected:
+
+ void openStream(AudioApi inputAudioApi, PerformanceMode inputPerfMode,
+ AudioApi outputAudioApi, PerformanceMode outputPerfMode) {
+ mOutputBuilder.setDirection(Direction::Output);
+ if (mOutputBuilder.isAAudioRecommended()) {
+ mOutputBuilder.setAudioApi(outputAudioApi);
+ }
+ mOutputBuilder.setPerformanceMode(outputPerfMode);
+ mOutputBuilder.setChannelCount(1);
+ mOutputBuilder.setFormat(AudioFormat::Float);
+ mOutputBuilder.setDataCallback(this);
+
+ Result r = mOutputBuilder.openStream(&mOutputStream);
+ ASSERT_EQ(r, Result::OK) << "Failed to open output stream " << convertToText(r);
+
+ mInputBuilder.setDirection(Direction::Input);
+ if (mInputBuilder.isAAudioRecommended()) {
+ mInputBuilder.setAudioApi(inputAudioApi);
+ }
+ mInputBuilder.setPerformanceMode(inputPerfMode);
+ mInputBuilder.setChannelCount(1);
+ mInputBuilder.setFormat(AudioFormat::Float);
+ mInputBuilder.setBufferCapacityInFrames(mOutputStream->getBufferCapacityInFrames() * 2);
+ mInputBuilder.setSampleRate(mOutputStream->getSampleRate());
+
+ r = mInputBuilder.openStream(&mInputStream);
+ ASSERT_EQ(r, Result::OK) << "Failed to open input stream " << convertToText(r);
+
+ setInputStream(mInputStream);
+ setOutputStream(mOutputStream);
+ }
+
+ void startStream() {
+ Result r = start();
+ ASSERT_EQ(r, Result::OK) << "Failed to start streams " << convertToText(r);
+ }
+
+ void stopStream() {
+ Result r = stop();
+ ASSERT_EQ(r, Result::OK) << "Failed to stop streams " << convertToText(r);
+ }
+
+ void closeStream() {
+ Result r = mOutputStream->close();
+ ASSERT_EQ(r, Result::OK) << "Failed to close output stream " << convertToText(r);
+ setOutputStream(nullptr);
+ r = mInputStream->close();
+ ASSERT_EQ(r, Result::OK) << "Failed to close input stream " << convertToText(r);
+ setInputStream(nullptr);
+ }
+
+ void checkXRuns() {
+ // Expect few xRuns with the use of full duplex stream
+ EXPECT_LT(mInputStream->getXRunCount().value(), 10);
+ EXPECT_LT(mOutputStream->getXRunCount().value(), 10);
+ }
+
+ void checkInputAndOutputBufferSizesMatch() {
+ // Expect the large majority of callbacks to have the same sized input and output
+ EXPECT_GE(mGoodCallbackCount, mCallbackCount * 9 / 10);
+ }
+
+ AudioStreamBuilder mInputBuilder;
+ AudioStreamBuilder mOutputBuilder;
+ AudioStream *mInputStream = nullptr;
+ AudioStream *mOutputStream = nullptr;
+ std::atomic<int32_t> mCallbackCount{0};
+ std::atomic<int32_t> mGoodCallbackCount{0};
+};
+
+TEST_P(TestFullDuplexStream, VerifyFullDuplexStream) {
+ const AudioApi inputAudioApi = std::get<0>(GetParam());
+ const PerformanceMode inputPerformanceMode = std::get<1>(GetParam());
+ const AudioApi outputAudioApi = std::get<2>(GetParam());
+ const PerformanceMode outputPerformanceMode = std::get<3>(GetParam());
+
+ openStream(inputAudioApi, inputPerformanceMode, outputAudioApi, outputPerformanceMode);
+ startStream();
+ usleep(kTimeToSleepMicros);
+ checkXRuns();
+ checkInputAndOutputBufferSizesMatch();
+ stopStream();
+ closeStream();
+}
+
+INSTANTIATE_TEST_SUITE_P(
+ TestFullDuplexStreamTest,
+ TestFullDuplexStream,
+ ::testing::Values(
+ TestFullDuplexStreamParams({AudioApi::AAudio, PerformanceMode::LowLatency,
+ AudioApi::AAudio, PerformanceMode::LowLatency}),
+ TestFullDuplexStreamParams({AudioApi::AAudio, PerformanceMode::LowLatency,
+ AudioApi::AAudio, PerformanceMode::None}),
+ TestFullDuplexStreamParams({AudioApi::AAudio, PerformanceMode::LowLatency,
+ AudioApi::AAudio, PerformanceMode::PowerSaving}),
+ TestFullDuplexStreamParams({AudioApi::AAudio, PerformanceMode::LowLatency,
+ AudioApi::OpenSLES, PerformanceMode::LowLatency}),
+ TestFullDuplexStreamParams({AudioApi::AAudio, PerformanceMode::LowLatency,
+ AudioApi::OpenSLES, PerformanceMode::None}),
+ TestFullDuplexStreamParams({AudioApi::AAudio, PerformanceMode::LowLatency,
+ AudioApi::OpenSLES, PerformanceMode::PowerSaving}),
+ TestFullDuplexStreamParams({AudioApi::AAudio, PerformanceMode::None,
+ AudioApi::AAudio, PerformanceMode::LowLatency}),
+ TestFullDuplexStreamParams({AudioApi::AAudio, PerformanceMode::None,
+ AudioApi::AAudio, PerformanceMode::None}),
+ TestFullDuplexStreamParams({AudioApi::AAudio, PerformanceMode::None,
+ AudioApi::AAudio, PerformanceMode::PowerSaving}),
+ TestFullDuplexStreamParams({AudioApi::AAudio, PerformanceMode::None,
+ AudioApi::OpenSLES, PerformanceMode::LowLatency}),
+ TestFullDuplexStreamParams({AudioApi::AAudio, PerformanceMode::None,
+ AudioApi::OpenSLES, PerformanceMode::None}),
+ TestFullDuplexStreamParams({AudioApi::AAudio, PerformanceMode::None,
+ AudioApi::OpenSLES, PerformanceMode::PowerSaving}),
+ TestFullDuplexStreamParams({AudioApi::AAudio, PerformanceMode::PowerSaving,
+ AudioApi::AAudio, PerformanceMode::LowLatency}),
+ TestFullDuplexStreamParams({AudioApi::AAudio, PerformanceMode::PowerSaving,
+ AudioApi::AAudio, PerformanceMode::None}),
+ TestFullDuplexStreamParams({AudioApi::AAudio, PerformanceMode::PowerSaving,
+ AudioApi::AAudio, PerformanceMode::PowerSaving}),
+ TestFullDuplexStreamParams({AudioApi::AAudio, PerformanceMode::PowerSaving,
+ AudioApi::OpenSLES, PerformanceMode::LowLatency}),
+ TestFullDuplexStreamParams({AudioApi::AAudio, PerformanceMode::PowerSaving,
+ AudioApi::OpenSLES, PerformanceMode::None}),
+ TestFullDuplexStreamParams({AudioApi::AAudio, PerformanceMode::PowerSaving,
+ AudioApi::OpenSLES, PerformanceMode::PowerSaving}),
+ TestFullDuplexStreamParams({AudioApi::OpenSLES, PerformanceMode::LowLatency,
+ AudioApi::AAudio, PerformanceMode::LowLatency}),
+ TestFullDuplexStreamParams({AudioApi::OpenSLES, PerformanceMode::LowLatency,
+ AudioApi::AAudio, PerformanceMode::None}),
+ TestFullDuplexStreamParams({AudioApi::OpenSLES, PerformanceMode::LowLatency,
+ AudioApi::AAudio, PerformanceMode::PowerSaving}),
+ TestFullDuplexStreamParams({AudioApi::OpenSLES, PerformanceMode::LowLatency,
+ AudioApi::OpenSLES, PerformanceMode::LowLatency}),
+ TestFullDuplexStreamParams({AudioApi::OpenSLES, PerformanceMode::LowLatency,
+ AudioApi::OpenSLES, PerformanceMode::None}),
+ TestFullDuplexStreamParams({AudioApi::OpenSLES, PerformanceMode::LowLatency,
+ AudioApi::OpenSLES, PerformanceMode::PowerSaving}),
+ TestFullDuplexStreamParams({AudioApi::OpenSLES, PerformanceMode::None,
+ AudioApi::AAudio, PerformanceMode::LowLatency}),
+ TestFullDuplexStreamParams({AudioApi::OpenSLES, PerformanceMode::None,
+ AudioApi::AAudio, PerformanceMode::None}),
+ TestFullDuplexStreamParams({AudioApi::OpenSLES, PerformanceMode::None,
+ AudioApi::AAudio, PerformanceMode::PowerSaving}),
+ TestFullDuplexStreamParams({AudioApi::OpenSLES, PerformanceMode::None,
+ AudioApi::OpenSLES, PerformanceMode::LowLatency}),
+ TestFullDuplexStreamParams({AudioApi::OpenSLES, PerformanceMode::None,
+ AudioApi::OpenSLES, PerformanceMode::None}),
+ TestFullDuplexStreamParams({AudioApi::OpenSLES, PerformanceMode::None,
+ AudioApi::OpenSLES, PerformanceMode::PowerSaving}),
+ TestFullDuplexStreamParams({AudioApi::OpenSLES, PerformanceMode::PowerSaving,
+ AudioApi::AAudio, PerformanceMode::LowLatency}),
+ TestFullDuplexStreamParams({AudioApi::OpenSLES, PerformanceMode::PowerSaving,
+ AudioApi::AAudio, PerformanceMode::None}),
+ TestFullDuplexStreamParams({AudioApi::OpenSLES, PerformanceMode::PowerSaving,
+ AudioApi::AAudio, PerformanceMode::PowerSaving}),
+ TestFullDuplexStreamParams({AudioApi::OpenSLES, PerformanceMode::PowerSaving,
+ AudioApi::OpenSLES, PerformanceMode::LowLatency}),
+ TestFullDuplexStreamParams({AudioApi::OpenSLES, PerformanceMode::PowerSaving,
+ AudioApi::OpenSLES, PerformanceMode::None}),
+ TestFullDuplexStreamParams({AudioApi::OpenSLES, PerformanceMode::PowerSaving,
+ AudioApi::OpenSLES, PerformanceMode::PowerSaving})
+ )
+);
diff --git a/tests/testResampler.cpp b/tests/testResampler.cpp
index de069419..5518829b 100644
--- a/tests/testResampler.cpp
+++ b/tests/testResampler.cpp
@@ -99,14 +99,22 @@ static void checkResampler(int32_t sourceRate, int32_t sinkRate,
}
}
+ // Flush out remaining frames from the flowgraph
+ while (!mcResampler->isWriteNeeded()) {
+ mcResampler->readNextFrame(output);
+ output++;
+ numRead++;
+ }
+
ASSERT_LE(numRead, kNumOutputSamples);
// Some frames are lost priming the FIR filter.
- const int kMaxAlgorithmicFrameLoss = 16;
+ const int kMaxAlgorithmicFrameLoss = 5;
EXPECT_GT(numRead, kNumOutputSamples - kMaxAlgorithmicFrameLoss);
int sinkZeroCrossingCount = countZeroCrossingsWithHysteresis(outputBuffer.get(), numRead);
- // Some cycles may get chopped off at the end.
- const int kMaxZeroCrossingDelta = 3;
+ // The sine wave may be cut off partially. This may cause multiple crossing
+ // differences when upsampling.
+ const int kMaxZeroCrossingDelta = std::max(sinkRate / sourceRate / 2, 1);
EXPECT_LE(abs(sourceZeroCrossingCount - sinkZeroCrossingCount), kMaxZeroCrossingDelta);
// Detect glitches by looking for spikes in the second derivative.
@@ -134,8 +142,7 @@ static void checkResampler(int32_t sourceRate, int32_t sinkRate,
TEST(test_resampler, resampler_scan_all) {
- // TODO Add 64000, 88200, 96000 when they work. Failing now.
- const int rates[] = {8000, 11025, 22050, 32000, 44100, 48000};
+ const int rates[] = {8000, 11025, 22050, 32000, 44100, 48000, 64000, 88200, 96000};
const MultiChannelResampler::Quality qualities[] =
{
MultiChannelResampler::Quality::Fastest,
@@ -191,10 +198,9 @@ TEST(test_resampler, resampler_11025_44100_best) {
checkResampler(11025, 44100, MultiChannelResampler::Quality::Best);
}
-// TODO This fails because the output is very low.
-//TEST(test_resampler, resampler_11025_88200_best) {
-// checkResampler(11025, 88200, MultiChannelResampler::Quality::Best);
-//}
+TEST(test_resampler, resampler_11025_88200_best) {
+ checkResampler(11025, 88200, MultiChannelResampler::Quality::Best);
+}
TEST(test_resampler, resampler_16000_48000_best) {
checkResampler(16000, 48000, MultiChannelResampler::Quality::Best);
diff --git a/tests/testStreamClosedMethods.cpp b/tests/testStreamClosedMethods.cpp
index a75e8aea..02defa4d 100644
--- a/tests/testStreamClosedMethods.cpp
+++ b/tests/testStreamClosedMethods.cpp
@@ -36,6 +36,17 @@ protected:
return (r == Result::OK);
}
+ bool releaseStream() {
+ Result r = mStream->release();
+ if (getSdkVersion() > __ANDROID_API_R__ && mBuilder.getAudioApi() != AudioApi::OpenSLES) {
+ EXPECT_EQ(r, Result::OK) << "Failed to release stream. " << convertToText(r);
+ return (r == Result::OK);
+ } else {
+ EXPECT_EQ(r, Result::ErrorUnimplemented) << "Did not get ErrorUnimplemented" << convertToText(r);
+ return (r == Result::ErrorUnimplemented);
+ }
+ }
+
bool closeStream() {
Result r = mStream->close();
EXPECT_EQ(r, Result::OK) << "Failed to close stream. " << convertToText(r);
@@ -59,7 +70,6 @@ protected:
return (time.tv_sec * (int64_t)1e9) + time.tv_nsec;
}
- int32_t mElapsedTimeMillis = 0; // used for passing back a value from a test function.
// ASSERT_* requires a void return type.
void measureCloseTime(int32_t delayMillis) {
ASSERT_TRUE(openStream());
@@ -71,20 +81,11 @@ protected:
int64_t stopTimeMillis = getNanoseconds() / 1e6;
int32_t elapsedTimeMillis = (int32_t)(stopTimeMillis - startTimeMillis);
ASSERT_GE(elapsedTimeMillis, delayMillis);
- mElapsedTimeMillis = elapsedTimeMillis;
}
void testDelayBeforeClose() {
- const int32_t delayMillis = 100;
- measureCloseTime(0);
- int32_t elapsedTimeMillis1 = mElapsedTimeMillis;
- // Do it again with a longer sleep using setDelayBeforeCloseMillis.
- // The increase in elapsed time should match the added delay.
+ const int32_t delayMillis = 500;
measureCloseTime(delayMillis);
- int32_t elapsedTimeMillis2 = mElapsedTimeMillis;
- int32_t extraElapsedTime = elapsedTimeMillis2 - elapsedTimeMillis1;
- // Expect the additional elapsed time to be close to the added delay.
- ASSERT_LE(abs(extraElapsedTime - delayMillis), delayMillis / 5);
}
AudioStreamBuilder mBuilder;
@@ -399,3 +400,33 @@ TEST_F(StreamClosedReturnValues, DelayBeforeCloseOutputOpenSL){
mBuilder.setDirection(Direction::Output);
testDelayBeforeClose();
}
+
+TEST_F(StreamClosedReturnValues, TestReleaseInput){
+ mBuilder.setDirection(Direction::Input);
+ ASSERT_TRUE(openStream());
+ ASSERT_TRUE(releaseStream());
+ ASSERT_TRUE(closeStream());
+}
+
+TEST_F(StreamClosedReturnValues, TestReleaseInputOpenSLES){
+ mBuilder.setAudioApi(AudioApi::OpenSLES);
+ mBuilder.setDirection(Direction::Input);
+ ASSERT_TRUE(openStream());
+ ASSERT_TRUE(releaseStream());
+ ASSERT_TRUE(closeStream());
+}
+
+TEST_F(StreamClosedReturnValues, TestReleaseOutput){
+ mBuilder.setDirection(Direction::Output);
+ ASSERT_TRUE(openStream());
+ ASSERT_TRUE(releaseStream());
+ ASSERT_TRUE(closeStream());
+}
+
+TEST_F(StreamClosedReturnValues, TestReleaseOutputOpenSLES){
+ mBuilder.setAudioApi(AudioApi::OpenSLES);
+ mBuilder.setDirection(Direction::Output);
+ ASSERT_TRUE(openStream());
+ ASSERT_TRUE(releaseStream());
+ ASSERT_TRUE(closeStream());
+}
diff --git a/tests/testStreamOpen.cpp b/tests/testStreamOpen.cpp
index 9d01c072..1c6b97f9 100644
--- a/tests/testStreamOpen.cpp
+++ b/tests/testStreamOpen.cpp
@@ -15,13 +15,19 @@
*/
#include <gtest/gtest.h>
+
+#include <aaudio/AAudioExtensions.h>
#include <oboe/Oboe.h>
-#include <android/api-level.h>
+#include <android/api-level.h>
#ifndef __ANDROID_API_S__
#define __ANDROID_API_S__ 31
#endif
+#ifndef __ANDROID_API_S_V2__
+#define __ANDROID_API_S_V2__ 32
+#endif
+
using namespace oboe;
class CallbackSizeMonitor : public AudioStreamCallback {
@@ -385,6 +391,29 @@ TEST_F(StreamOpenOutput, LowLatencyStreamHasSmallBufferSize){
}
}
+// Make sure the parameters get copied from the child stream.
+TEST_F(StreamOpenOutput, AAudioOutputSampleRate44100FilterConfiguration) {
+ if (mBuilder.isAAudioRecommended()) {
+ mBuilder.setDirection(Direction::Output);
+ mBuilder.setPerformanceMode(PerformanceMode::LowLatency);
+ mBuilder.setSharingMode(SharingMode::Exclusive);
+ // Try to force the use of a FilterAudioStream by requesting conversion.
+ mBuilder.setSampleRate(44100);
+ mBuilder.setSampleRateConversionQuality(SampleRateConversionQuality::Medium);
+ ASSERT_TRUE(openStream());
+ if (getSdkVersion() >= __ANDROID_API_U__) {
+ ASSERT_LT(0, mStream->getHardwareSampleRate());
+ ASSERT_LT(0, mStream->getHardwareChannelCount());
+ ASSERT_LT(0, (int)mStream->getHardwareFormat());
+ }
+ // If MMAP is not supported then we cannot get an EXCLUSIVE mode stream.
+ if (!AAudioExtensions::getInstance().isMMapSupported()) {
+ ASSERT_NE(SharingMode::Exclusive, mStream->getSharingMode()); // IMPOSSIBLE
+ }
+ ASSERT_TRUE(closeStream());
+ }
+}
+
// See if sample rate conversion by Oboe is calling the callback.
TEST_F(StreamOpenOutput, AAudioOutputSampleRate44100) {
checkSampleRateConversionAdvancing(Direction::Output);
@@ -437,6 +466,98 @@ TEST_F(StreamOpenInput, AAudioInputSetAttributionTag){
}
}
+TEST_F(StreamOpenInput, AAudioInputSetSpatializationBehavior) {
+ mBuilder.setDirection(Direction::Input);
+ mBuilder.setSpatializationBehavior(SpatializationBehavior::Auto);
+ ASSERT_TRUE(openStream());
+ if (getSdkVersion() >= __ANDROID_API_S_V2__){
+ ASSERT_EQ(mStream->getSpatializationBehavior(), SpatializationBehavior::Auto);
+ } else {
+ ASSERT_EQ(mStream->getSpatializationBehavior(), SpatializationBehavior::Never);
+ }
+ ASSERT_TRUE(closeStream());
+}
+
+TEST_F(StreamOpenOutput, AAudioOutputSetSpatializationBehavior) {
+ mBuilder.setDirection(Direction::Output);
+ mBuilder.setSpatializationBehavior(SpatializationBehavior::Never);
+ ASSERT_TRUE(openStream());
+ ASSERT_EQ(mStream->getSpatializationBehavior(), SpatializationBehavior::Never);
+ ASSERT_TRUE(closeStream());
+}
+
+TEST_F(StreamOpenOutput, OpenSLESOutputSetSpatializationBehavior) {
+ mBuilder.setDirection(Direction::Output);
+ mBuilder.setAudioApi(AudioApi::OpenSLES);
+ mBuilder.setSpatializationBehavior(SpatializationBehavior::Auto);
+ ASSERT_TRUE(openStream());
+ ASSERT_EQ(mStream->getSpatializationBehavior(), SpatializationBehavior::Never);
+ ASSERT_TRUE(closeStream());
+}
+
+TEST_F(StreamOpenInput, AAudioInputSetSpatializationBehaviorUnspecified) {
+ mBuilder.setDirection(Direction::Input);
+ mBuilder.setSpatializationBehavior(SpatializationBehavior::Unspecified);
+ ASSERT_TRUE(openStream());
+ ASSERT_EQ(mStream->getSpatializationBehavior(), SpatializationBehavior::Never);
+ ASSERT_TRUE(closeStream());
+}
+
+TEST_F(StreamOpenOutput, AAudioOutputSetSpatializationBehaviorUnspecified) {
+ mBuilder.setDirection(Direction::Output);
+ mBuilder.setSpatializationBehavior(SpatializationBehavior::Unspecified);
+ ASSERT_TRUE(openStream());
+ ASSERT_EQ(mStream->getSpatializationBehavior(), SpatializationBehavior::Never);
+ ASSERT_TRUE(closeStream());
+}
+
+TEST_F(StreamOpenInput, AAudioInputSetIsContentSpatialized) {
+ mBuilder.setDirection(Direction::Input);
+ mBuilder.setIsContentSpatialized(true);
+ ASSERT_TRUE(openStream());
+ ASSERT_EQ(mStream->isContentSpatialized(), true);
+ ASSERT_TRUE(closeStream());
+}
+
+TEST_F(StreamOpenOutput, AAudioOutputSetIsContentSpatialized) {
+ mBuilder.setDirection(Direction::Output);
+ mBuilder.setIsContentSpatialized(true);
+ ASSERT_TRUE(openStream());
+ ASSERT_EQ(mStream->isContentSpatialized(), true);
+ ASSERT_TRUE(closeStream());
+}
+
+TEST_F(StreamOpenOutput, OpenSLESOutputSetIsContentSpatialized) {
+ mBuilder.setDirection(Direction::Output);
+ mBuilder.setAudioApi(AudioApi::OpenSLES);
+ mBuilder.setIsContentSpatialized(true);
+ ASSERT_TRUE(openStream());
+ ASSERT_EQ(mStream->isContentSpatialized(), true);
+ ASSERT_TRUE(closeStream());
+}
+
+TEST_F(StreamOpenOutput, AAudioOutputSetIsContentSpatializedFalse) {
+ mBuilder.setDirection(Direction::Output);
+ mBuilder.setIsContentSpatialized(false);
+ ASSERT_TRUE(openStream());
+ ASSERT_EQ(mStream->isContentSpatialized(), false);
+ ASSERT_TRUE(closeStream());
+}
+
+TEST_F(StreamOpenOutput, AAudioOutputSetIsContentSpatializedUnspecified) {
+ mBuilder.setDirection(Direction::Output);
+ ASSERT_TRUE(openStream());
+ ASSERT_EQ(mStream->isContentSpatialized(), false);
+ ASSERT_TRUE(closeStream());
+}
+
+TEST_F(StreamOpenInput, AAudioInputSetIsContentSpatializedUnspecified) {
+ mBuilder.setDirection(Direction::Input);
+ ASSERT_TRUE(openStream());
+ ASSERT_EQ(mStream->isContentSpatialized(), false);
+ ASSERT_TRUE(closeStream());
+}
+
TEST_F(StreamOpenOutput, OutputForOpenSLESPerformanceModeNoneGetBufferSizeInFrames){
mBuilder.setPerformanceMode(PerformanceMode::None);
mBuilder.setAudioApi(AudioApi::OpenSLES);
@@ -460,3 +581,165 @@ TEST_F(StreamOpenOutput, OboeExtensions){
ASSERT_TRUE(OboeExtensions::isMMapEnabled());
}
}
+
+TEST_F(StreamOpenInput, AAudioInputSetPrivacySensitiveModeUnspecifiedUnprocessed){
+ if (getSdkVersion() >= __ANDROID_API_R__){
+ mBuilder.setDirection(Direction::Input);
+ mBuilder.setAudioApi(AudioApi::AAudio);
+ mBuilder.setInputPreset(InputPreset::Unprocessed);
+ ASSERT_TRUE(openStream());
+ ASSERT_EQ(mStream->getPrivacySensitiveMode(), PrivacySensitiveMode::Disabled);
+ ASSERT_TRUE(closeStream());
+ }
+}
+
+TEST_F(StreamOpenInput, AAudioInputSetPrivacySensitiveModeUnspecifiedVoiceCommunication){
+ if (getSdkVersion() >= __ANDROID_API_R__){
+ mBuilder.setDirection(Direction::Input);
+ mBuilder.setAudioApi(AudioApi::AAudio);
+ mBuilder.setInputPreset(InputPreset::VoiceCommunication);
+ ASSERT_TRUE(openStream());
+ ASSERT_EQ(mStream->getPrivacySensitiveMode(), PrivacySensitiveMode::Enabled);
+ ASSERT_TRUE(closeStream());
+ }
+}
+
+TEST_F(StreamOpenInput, AAudioInputSetPrivacySensitiveModeVoiceDisabled){
+ if (getSdkVersion() >= __ANDROID_API_R__){
+ mBuilder.setDirection(Direction::Input);
+ mBuilder.setAudioApi(AudioApi::AAudio);
+ mBuilder.setInputPreset(InputPreset::VoiceCommunication);
+ mBuilder.setPrivacySensitiveMode(PrivacySensitiveMode::Disabled);
+ ASSERT_TRUE(openStream());
+ ASSERT_EQ(mStream->getPrivacySensitiveMode(), PrivacySensitiveMode::Disabled);
+ ASSERT_TRUE(closeStream());
+ }
+}
+
+TEST_F(StreamOpenInput, AAudioInputSetPrivacySensitiveModeUnprocessedEnabled){
+ if (getSdkVersion() >= __ANDROID_API_R__){
+ mBuilder.setDirection(Direction::Input);
+ mBuilder.setAudioApi(AudioApi::AAudio);
+ mBuilder.setInputPreset(InputPreset::Unprocessed);
+ mBuilder.setPrivacySensitiveMode(PrivacySensitiveMode::Enabled);
+ ASSERT_TRUE(openStream());
+ ASSERT_EQ(mStream->getPrivacySensitiveMode(), PrivacySensitiveMode::Enabled);
+ ASSERT_TRUE(closeStream());
+ }
+}
+
+TEST_F(StreamOpenOutput, AAudioOutputSetPrivacySensitiveModeGetsUnspecified){
+ if (getSdkVersion() >= __ANDROID_API_R__){
+ mBuilder.setDirection(Direction::Output);
+ mBuilder.setAudioApi(AudioApi::AAudio);
+ mBuilder.setPrivacySensitiveMode(PrivacySensitiveMode::Enabled);
+ ASSERT_TRUE(openStream());
+ ASSERT_EQ(mStream->getPrivacySensitiveMode(), PrivacySensitiveMode::Unspecified);
+ ASSERT_TRUE(closeStream());
+ }
+}
+
+TEST_F(StreamOpenInput, OpenSLESInputSetPrivacySensitiveModeDoesNotCrash){
+ mBuilder.setDirection(Direction::Input);
+ mBuilder.setAudioApi(AudioApi::OpenSLES);
+ mBuilder.setInputPreset(InputPreset::Unprocessed);
+ mBuilder.setPrivacySensitiveMode(PrivacySensitiveMode::Enabled);
+ ASSERT_TRUE(openStream());
+ ASSERT_EQ(mStream->getPrivacySensitiveMode(), PrivacySensitiveMode::Unspecified);
+ ASSERT_TRUE(closeStream());
+}
+
+TEST_F(StreamOpenInput, OldAndroidVersionInputSetPrivacySensitiveModeDoesNotCrash){
+ if (getSdkVersion() < __ANDROID_API_R__) {
+ mBuilder.setDirection(Direction::Input);
+ mBuilder.setInputPreset(InputPreset::Unprocessed);
+ mBuilder.setPrivacySensitiveMode(PrivacySensitiveMode::Enabled);
+ ASSERT_TRUE(openStream());
+ ASSERT_EQ(mStream->getPrivacySensitiveMode(), PrivacySensitiveMode::Unspecified);
+ ASSERT_TRUE(closeStream());
+ }
+}
+
+TEST_F(StreamOpenOutput, AAudioOutputSetAllowedCapturePolicyUnspecifiedGetsAll){
+ if (getSdkVersion() >= __ANDROID_API_Q__){
+ mBuilder.setDirection(Direction::Output);
+ mBuilder.setAudioApi(AudioApi::AAudio);
+ mBuilder.setAllowedCapturePolicy(AllowedCapturePolicy::Unspecified);
+ ASSERT_TRUE(openStream());
+ ASSERT_EQ(mStream->getAllowedCapturePolicy(), AllowedCapturePolicy::All);
+ ASSERT_TRUE(closeStream());
+ }
+}
+
+TEST_F(StreamOpenOutput, AAudioOutputSetAllowedCapturePolicyAll){
+ if (getSdkVersion() >= __ANDROID_API_Q__){
+ mBuilder.setDirection(Direction::Output);
+ mBuilder.setAudioApi(AudioApi::AAudio);
+ mBuilder.setAllowedCapturePolicy(AllowedCapturePolicy::All);
+ ASSERT_TRUE(openStream());
+ ASSERT_EQ(mStream->getAllowedCapturePolicy(), AllowedCapturePolicy::All);
+ ASSERT_TRUE(closeStream());
+ }
+}
+
+TEST_F(StreamOpenOutput, AAudioOutputSetAllowedCapturePolicySystem){
+ if (getSdkVersion() >= __ANDROID_API_Q__){
+ mBuilder.setDirection(Direction::Output);
+ mBuilder.setAudioApi(AudioApi::AAudio);
+ mBuilder.setAllowedCapturePolicy(AllowedCapturePolicy::System);
+ ASSERT_TRUE(openStream());
+ ASSERT_EQ(mStream->getAllowedCapturePolicy(), AllowedCapturePolicy::System);
+ ASSERT_TRUE(closeStream());
+ }
+}
+
+TEST_F(StreamOpenOutput, AAudioOutputSetAllowedCapturePolicyNone){
+ if (getSdkVersion() >= __ANDROID_API_Q__){
+ mBuilder.setDirection(Direction::Output);
+ mBuilder.setAudioApi(AudioApi::AAudio);
+ mBuilder.setAllowedCapturePolicy(AllowedCapturePolicy::None);
+ ASSERT_TRUE(openStream());
+ ASSERT_EQ(mStream->getAllowedCapturePolicy(), AllowedCapturePolicy::None);
+ ASSERT_TRUE(closeStream());
+ }
+}
+
+TEST_F(StreamOpenOutput, AAudioOutputDoNotSetAllowedCapturePolicy){
+ mBuilder.setDirection(Direction::Output);
+ mBuilder.setAudioApi(AudioApi::AAudio);
+ ASSERT_TRUE(openStream());
+ if (getSdkVersion() >= __ANDROID_API_Q__){
+ ASSERT_EQ(mStream->getAllowedCapturePolicy(), AllowedCapturePolicy::All);
+ } else {
+ ASSERT_EQ(mStream->getAllowedCapturePolicy(), AllowedCapturePolicy::Unspecified);
+ }
+ ASSERT_TRUE(closeStream());
+}
+
+TEST_F(StreamOpenOutput, OpenSLESOutputSetAllowedCapturePolicyAllGetsUnspecified){
+ mBuilder.setDirection(Direction::Output);
+ mBuilder.setAudioApi(AudioApi::OpenSLES);
+ mBuilder.setAllowedCapturePolicy(AllowedCapturePolicy::All);
+ ASSERT_TRUE(openStream());
+ ASSERT_EQ(mStream->getAllowedCapturePolicy(), AllowedCapturePolicy::Unspecified);
+ ASSERT_TRUE(closeStream());
+}
+
+TEST_F(StreamOpenOutput, AAudioBeforeQOutputSetAllowedCapturePolicyAllGetsUnspecified){
+ if (getSdkVersion() < __ANDROID_API_Q__){
+ mBuilder.setDirection(Direction::Output);
+ mBuilder.setAudioApi(AudioApi::AAudio);
+ mBuilder.setAllowedCapturePolicy(AllowedCapturePolicy::All);
+ ASSERT_TRUE(openStream());
+ ASSERT_EQ(mStream->getAllowedCapturePolicy(), AllowedCapturePolicy::Unspecified);
+ ASSERT_TRUE(closeStream());
+ }
+}
+
+TEST_F(StreamOpenInput, AAudioInputSetAllowedCapturePolicyAllGetsUnspecified){
+ mBuilder.setDirection(Direction::Input);
+ mBuilder.setAllowedCapturePolicy(AllowedCapturePolicy::All);
+ ASSERT_TRUE(openStream());
+ ASSERT_EQ(mStream->getAllowedCapturePolicy(), AllowedCapturePolicy::Unspecified);
+ ASSERT_TRUE(closeStream());
+}