aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPhil Burk <philburk@mobileer.com>2022-11-23 15:25:04 -0800
committerPhil Burk <philburk@mobileer.com>2022-11-23 15:25:04 -0800
commit9e6551e498c3231a8d65b38e8fff1d505f96ce41 (patch)
tree8472a32a918907f9df4392ab78fc1eb115b6e40f
parent11ea86592ef8312c9a5cf6debf29809c3775dc50 (diff)
parent368a9e57e943af62372f1ea753d879df1ea48481 (diff)
downloadoboe-9e6551e498c3231a8d65b38e8fff1d505f96ce41.tar.gz
Merge branch 'min_app_kt'
-rw-r--r--samples/LiveEffect/build.gradle2
-rw-r--r--samples/MegaDrone/build.gradle2
-rw-r--r--samples/README.md14
-rw-r--r--samples/RhythmGame/build.gradle2
-rw-r--r--samples/SoundBoard/build.gradle4
-rw-r--r--samples/build.gradle13
-rw-r--r--samples/drumthumper/build.gradle16
-rw-r--r--samples/hello-oboe/build.gradle2
-rw-r--r--samples/minimaloboe/.gitignore1
-rw-r--r--samples/minimaloboe/README.md16
-rw-r--r--samples/minimaloboe/build.gradle76
-rw-r--r--samples/minimaloboe/proguard-rules.pro21
-rw-r--r--samples/minimaloboe/src/main/AndroidManifest.xml25
-rw-r--r--samples/minimaloboe/src/main/cpp/CMakeLists.txt52
-rw-r--r--samples/minimaloboe/src/main/cpp/MinimalOboeJNI.cpp66
-rw-r--r--samples/minimaloboe/src/main/cpp/SimpleNoiseMaker.cpp92
-rw-r--r--samples/minimaloboe/src/main/cpp/SimpleNoiseMaker.h71
-rw-r--r--samples/minimaloboe/src/main/java/com/example/minimaloboe/AudioPlayer.kt87
-rw-r--r--samples/minimaloboe/src/main/java/com/example/minimaloboe/MainActivity.kt116
-rw-r--r--samples/minimaloboe/src/main/java/com/example/minimaloboe/ui/theme/Color.kt8
-rw-r--r--samples/minimaloboe/src/main/java/com/example/minimaloboe/ui/theme/Shape.kt11
-rw-r--r--samples/minimaloboe/src/main/java/com/example/minimaloboe/ui/theme/Theme.kt44
-rw-r--r--samples/minimaloboe/src/main/java/com/example/minimaloboe/ui/theme/Type.kt21
-rw-r--r--samples/minimaloboe/src/main/res/drawable-v24/ic_launcher_foreground.xml30
-rw-r--r--samples/minimaloboe/src/main/res/drawable/ic_launcher_background.xml170
-rw-r--r--samples/minimaloboe/src/main/res/mipmap-anydpi-v26/ic_launcher.xml5
-rw-r--r--samples/minimaloboe/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml5
-rw-r--r--samples/minimaloboe/src/main/res/mipmap-hdpi/ic_launcher.webpbin0 -> 1404 bytes
-rw-r--r--samples/minimaloboe/src/main/res/mipmap-hdpi/ic_launcher_round.webpbin0 -> 2898 bytes
-rw-r--r--samples/minimaloboe/src/main/res/mipmap-mdpi/ic_launcher.webpbin0 -> 982 bytes
-rw-r--r--samples/minimaloboe/src/main/res/mipmap-mdpi/ic_launcher_round.webpbin0 -> 1772 bytes
-rw-r--r--samples/minimaloboe/src/main/res/mipmap-xhdpi/ic_launcher.webpbin0 -> 1900 bytes
-rw-r--r--samples/minimaloboe/src/main/res/mipmap-xhdpi/ic_launcher_round.webpbin0 -> 3918 bytes
-rw-r--r--samples/minimaloboe/src/main/res/mipmap-xxhdpi/ic_launcher.webpbin0 -> 2884 bytes
-rw-r--r--samples/minimaloboe/src/main/res/mipmap-xxhdpi/ic_launcher_round.webpbin0 -> 5914 bytes
-rw-r--r--samples/minimaloboe/src/main/res/mipmap-xxxhdpi/ic_launcher.webpbin0 -> 3844 bytes
-rw-r--r--samples/minimaloboe/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webpbin0 -> 7778 bytes
-rw-r--r--samples/minimaloboe/src/main/res/values/colors.xml10
-rw-r--r--samples/minimaloboe/src/main/res/values/strings.xml3
-rw-r--r--samples/minimaloboe/src/main/res/values/themes.xml7
-rw-r--r--samples/settings.gradle1
41 files changed, 969 insertions, 24 deletions
diff --git a/samples/LiveEffect/build.gradle b/samples/LiveEffect/build.gradle
index 5b491b2f..f336d2e2 100644
--- a/samples/LiveEffect/build.gradle
+++ b/samples/LiveEffect/build.gradle
@@ -32,7 +32,7 @@ android {
}
dependencies {
- implementation 'androidx.appcompat:appcompat:1.0.0-rc02'
+ implementation 'androidx.appcompat:appcompat:1.6.0-rc01'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation project(':audio-device')
}
diff --git a/samples/MegaDrone/build.gradle b/samples/MegaDrone/build.gradle
index 181b65fb..93c372c0 100644
--- a/samples/MegaDrone/build.gradle
+++ b/samples/MegaDrone/build.gradle
@@ -42,6 +42,6 @@ android {
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
- implementation 'androidx.appcompat:appcompat:1.0.0-rc02'
+ implementation 'androidx.appcompat:appcompat:1.6.0-rc01'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
}
diff --git a/samples/README.md b/samples/README.md
index e5e46c4b..bfee3de5 100644
--- a/samples/README.md
+++ b/samples/README.md
@@ -2,13 +2,15 @@ Oboe Samples
==============
These samples demonstrate how to use the Oboe library:
-1. [hello-oboe](hello-oboe): creates an output (playback) stream and plays a
-sine wave when you tap the screen
+1. [MinimalOboe](minimaloboe): Just create an Oboe stream and play white noise. Restart stream when disconnected. (Kotlin/Compose)
+1. [hello-oboe](hello-oboe): Creates an output (playback) stream and plays a
+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.
-1. [MegaDrone](MegaDrone): A one hundred oscillator synthesizer, demonstrates low latency and CPU performance
-1. [LiveEffect](LiveEffect): loops audio from input stream to output stream to demonstrate duplex capability
-1. [SoundBoard](SoundBoard): A 30 note dynamic synthesizer, demonstating combining signals.
+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. [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)
Pre-requisites
-------------
diff --git a/samples/RhythmGame/build.gradle b/samples/RhythmGame/build.gradle
index 39114f42..88681f43 100644
--- a/samples/RhythmGame/build.gradle
+++ b/samples/RhythmGame/build.gradle
@@ -63,6 +63,6 @@ android {
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
- implementation 'androidx.appcompat:appcompat:1.0.0-rc02'
+ implementation 'androidx.appcompat:appcompat:1.6.0-rc01'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
}
diff --git a/samples/SoundBoard/build.gradle b/samples/SoundBoard/build.gradle
index a2d125fc..81cb6050 100644
--- a/samples/SoundBoard/build.gradle
+++ b/samples/SoundBoard/build.gradle
@@ -45,6 +45,6 @@ android {
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
- implementation 'androidx.appcompat:appcompat:1.0.0-rc02'
- implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
+ implementation 'androidx.appcompat:appcompat:1.6.0-rc01'
+ implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
}
diff --git a/samples/build.gradle b/samples/build.gradle
index 5ac1c6aa..6b8dbbf9 100644
--- a/samples/build.gradle
+++ b/samples/build.gradle
@@ -17,19 +17,20 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
-
-
buildscript {
- ext.kotlin_version = '1.3.50'
+ ext {
+ compose_version = '1.2.0'
+ kotlin_version = '1.7.0'
+ }
repositories {
google()
- jcenter()
+ mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.2.2'
// NOTE: Do not place your application dependencies here; they belong
- // in the individual module build.gradle files
+ // in the individual module build.gradle files.
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
@@ -37,6 +38,6 @@ buildscript {
allprojects {
repositories {
google()
- jcenter()
+ mavenCentral()
}
}
diff --git a/samples/drumthumper/build.gradle b/samples/drumthumper/build.gradle
index 29a668b2..17a11990 100644
--- a/samples/drumthumper/build.gradle
+++ b/samples/drumthumper/build.gradle
@@ -1,6 +1,7 @@
-apply plugin: 'com.android.application'
-apply plugin: 'kotlin-android'
-apply plugin: 'kotlin-android-extensions'
+plugins {
+ id 'com.android.application'
+ id 'org.jetbrains.kotlin.android'
+}
android {
compileSdkVersion 33
@@ -35,9 +36,12 @@ android {
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
- implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
- implementation 'androidx.appcompat:appcompat:1.2.0'
- implementation 'androidx.core:core-ktx:1.3.1'
+ implementation "androidx.core:core-ktx:$kotlin_version"
+ implementation 'androidx.appcompat:appcompat:1.6.0-rc01'
+ def lifecycle_version = "2.5.1"
+ implementation "androidx.lifecycle:lifecycle-viewmodel:$lifecycle_version"
+ implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
implementation project(path: ':iolib')
implementation project(path: ':parselib')
}
+
diff --git a/samples/hello-oboe/build.gradle b/samples/hello-oboe/build.gradle
index 750efc82..29b6fc26 100644
--- a/samples/hello-oboe/build.gradle
+++ b/samples/hello-oboe/build.gradle
@@ -35,6 +35,6 @@ android {
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation project(':audio-device')
- implementation 'androidx.appcompat:appcompat:1.0.0-rc02'
+ implementation 'androidx.appcompat:appcompat:1.6.0-rc01'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
}
diff --git a/samples/minimaloboe/.gitignore b/samples/minimaloboe/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/samples/minimaloboe/.gitignore
@@ -0,0 +1 @@
+/build \ No newline at end of file
diff --git a/samples/minimaloboe/README.md b/samples/minimaloboe/README.md
new file mode 100644
index 00000000..4d080edd
--- /dev/null
+++ b/samples/minimaloboe/README.md
@@ -0,0 +1,16 @@
+# Minimal Oboe
+
+## Overview
+
+This app is a very simple demonstration of turning on audio from buttons.
+It uses a low-latency Oboe stream.
+
+## Implementation
+
+The app is written using Kotlin and Jetpack Compose.
+
+The app state is maintained by subclassing DefaultLifecycleObserver.
+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.
diff --git a/samples/minimaloboe/build.gradle b/samples/minimaloboe/build.gradle
new file mode 100644
index 00000000..bf03f789
--- /dev/null
+++ b/samples/minimaloboe/build.gradle
@@ -0,0 +1,76 @@
+plugins {
+ id 'com.android.application'
+ id 'org.jetbrains.kotlin.android'
+}
+
+android {
+ compileSdk 33
+
+ defaultConfig {
+ applicationId "com.example.minimaloboe"
+ minSdk 21
+ targetSdk 32
+ versionCode 1
+ versionName "1.0"
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ vectorDrawables {
+ useSupportLibrary true
+ }
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = '1.8'
+ }
+ buildFeatures {
+ compose true
+ }
+ composeOptions {
+ kotlinCompilerExtensionVersion compose_version
+ }
+
+ externalNativeBuild {
+ cmake {
+ path 'src/main/cpp/CMakeLists.txt'
+ }
+ }
+ packagingOptions {
+ resources {
+ excludes += '/META-INF/{AL2.0,LGPL2.1}'
+ }
+ }
+}
+
+dependencies {
+ implementation "androidx.core:core-ktx:$kotlin_version"
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4"
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4"
+ implementation "androidx.activity:activity-ktx:1.6.0"
+ def lifecycle_version = "2.6.0-alpha02"
+ implementation "androidx.lifecycle:lifecycle-process:$lifecycle_version"
+ implementation "androidx.lifecycle:lifecycle-viewmodel:$lifecycle_version"
+ implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_version"
+ implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
+ implementation "androidx.lifecycle:lifecycle-runtime-compose:$lifecycle_version"
+ implementation "androidx.compose.ui:ui:$compose_version"
+ implementation "androidx.compose.material:material:$compose_version"
+ implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
+ implementation 'androidx.activity:activity-compose:1.3.1'
+ implementation 'androidx.appcompat:appcompat:1.6.0-rc01'
+ testImplementation 'junit:junit:4.13.2'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.3'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
+ androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
+ debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"
+ debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version"
+}
diff --git a/samples/minimaloboe/proguard-rules.pro b/samples/minimaloboe/proguard-rules.pro
new file mode 100644
index 00000000..f1b42451
--- /dev/null
+++ b/samples/minimaloboe/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/samples/minimaloboe/src/main/AndroidManifest.xml b/samples/minimaloboe/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..25d9e02c
--- /dev/null
+++ b/samples/minimaloboe/src/main/AndroidManifest.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.example.minimaloboe">
+
+ <application
+ android:allowBackup="true"
+ android:icon="@mipmap/ic_launcher"
+ android:label="@string/app_name"
+ android:roundIcon="@mipmap/ic_launcher_round"
+ android:supportsRtl="true"
+ android:theme="@style/Theme.Samples">
+ <activity
+ android:name=".MainActivity"
+ android:exported="true"
+ android:label="@string/app_name"
+ android:theme="@style/Theme.Samples">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ </application>
+
+</manifest>
diff --git a/samples/minimaloboe/src/main/cpp/CMakeLists.txt b/samples/minimaloboe/src/main/cpp/CMakeLists.txt
new file mode 100644
index 00000000..045ff5c5
--- /dev/null
+++ b/samples/minimaloboe/src/main/cpp/CMakeLists.txt
@@ -0,0 +1,52 @@
+#
+# Copyright 2022 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.
+#
+
+cmake_minimum_required(VERSION 3.4.1)
+
+# Set the path to the Oboe library directory
+set (OBOE_DIR ../../../../../)
+#message("OBOE_DIR = " + ${OBOE_DIR})
+
+add_subdirectory(${OBOE_DIR} ./oboe-bin)
+
+# include folders
+include_directories(
+ ${OBOE_DIR}/include
+ ${CMAKE_CURRENT_LIST_DIR}
+)
+
+# App specific sources
+set (APP_SOURCES
+ SimpleNoiseMaker.cpp
+ MinimalOboeJNI.cpp
+ )
+
+# Build the minimaloboe (native) library
+add_library(minimaloboe SHARED
+ ${APP_SOURCES}
+ )
+
+# Enable optimization flags: if having problems with source level debugging,
+# disable -Ofast ( and debug ), re-enable after done debugging.
+target_compile_options(minimaloboe PRIVATE -Wall -Werror "$<$<CONFIG:RELEASE>:-Ofast>")
+
+target_link_libraries( # Specifies the target library.
+ minimaloboe
+ oboe
+
+ # Links the target library to the log library
+ # included in the NDK.
+ log)
diff --git a/samples/minimaloboe/src/main/cpp/MinimalOboeJNI.cpp b/samples/minimaloboe/src/main/cpp/MinimalOboeJNI.cpp
new file mode 100644
index 00000000..295c9efa
--- /dev/null
+++ b/samples/minimaloboe/src/main/cpp/MinimalOboeJNI.cpp
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2022 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 <jni.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+static const char *TAG = "MinimalOboeJNI";
+
+#include <android/log.h>
+
+#include <SimpleNoiseMaker.h>
+
+// JNI functions are "C" calling convention
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+using namespace oboe;
+
+// Use a static object so we don't have to worry about it getting deleted at the wrong time.
+static SimpleNoiseMaker sPlayer;
+
+/**
+ * Native (JNI) implementation of AudioPlayer.startAudiostreamNative()
+ */
+JNIEXPORT jint JNICALL Java_com_example_minimaloboe_AudioPlayer_startAudioStreamNative(
+ JNIEnv * /* env */, jobject) {
+ __android_log_print(ANDROID_LOG_INFO, TAG, "%s", __func__);
+ Result result = sPlayer.open();
+ if (result == Result::OK) {
+ result = sPlayer.start();
+ }
+ return (jint) result;
+}
+
+/**
+ * Native (JNI) implementation of AudioPlayer.stopAudioStreamNative()
+ */
+JNIEXPORT jint JNICALL Java_com_example_minimaloboe_AudioPlayer_stopAudioStreamNative(
+ JNIEnv * /* env */, jobject) {
+ __android_log_print(ANDROID_LOG_INFO, TAG, "%s", __func__);
+ // We need to close() even if the stop() fails because we need to delete the resources.
+ Result result1 = sPlayer.stop();
+ Result result2 = sPlayer.close();
+ // Return first failure code.
+ return (jint) ((result1 != Result::OK) ? result1 : result2);
+}
+#ifdef __cplusplus
+}
+#endif
diff --git a/samples/minimaloboe/src/main/cpp/SimpleNoiseMaker.cpp b/samples/minimaloboe/src/main/cpp/SimpleNoiseMaker.cpp
new file mode 100644
index 00000000..40583917
--- /dev/null
+++ b/samples/minimaloboe/src/main/cpp/SimpleNoiseMaker.cpp
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2022 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>
+
+static const char *TAG = "SimpleNoiseMaker";
+
+#include <android/log.h>
+
+#include <SimpleNoiseMaker.h>
+
+using namespace oboe;
+
+oboe::Result SimpleNoiseMaker::open() {
+ // Use shared_ptr to prevent use of a deleted callback.
+ mDataCallback = std::make_shared<MyDataCallback>();
+ mErrorCallback = std::make_shared<MyErrorCallback>(this);
+
+ AudioStreamBuilder builder;
+ oboe::Result result = builder.setSharingMode(oboe::SharingMode::Exclusive)
+ ->setPerformanceMode(oboe::PerformanceMode::LowLatency)
+ ->setFormat(oboe::AudioFormat::Float)
+ ->setChannelCount(kChannelCount)
+ ->setDataCallback(mDataCallback)
+ ->setErrorCallback(mErrorCallback)
+ // Open using a shared_ptr.
+ ->openStream(mStream);
+ return result;
+}
+
+oboe::Result SimpleNoiseMaker::start() {
+ return mStream->requestStart();
+}
+
+oboe::Result SimpleNoiseMaker::stop() {
+ return mStream->requestStop();
+}
+
+oboe::Result SimpleNoiseMaker::close() {
+ return mStream->close();
+}
+
+/**
+ * This callback method will be called from a high priority audio thread.
+ * It should only do math and not do any blocking operations like
+ * reading or writing files, memory allocation, or networking.
+ * @param audioStream
+ * @param audioData pointer to an array of samples to be filled
+ * @param numFrames number of frames needed
+ * @return
+ */
+DataCallbackResult SimpleNoiseMaker::MyDataCallback::onAudioReady(
+ AudioStream *audioStream,
+ void *audioData,
+ int32_t numFrames) {
+ // We requested float when we built the stream.
+ float *output = (float *) audioData;
+ // Fill buffer with random numbers to create "white noise".
+ int numSamples = numFrames * kChannelCount;
+ for (int i = 0; i < numSamples; i++) {
+ // drand48() returns a random number between 0.0 and 1.0.
+ // Center and scale it to a reasonable value.
+ *output++ = (float) ((drand48() - 0.5) * 0.6);
+ }
+ return oboe::DataCallbackResult::Continue;
+}
+
+void SimpleNoiseMaker::MyErrorCallback::onErrorAfterClose(oboe::AudioStream *oboeStream,
+ oboe::Result error) {
+ __android_log_print(ANDROID_LOG_INFO, TAG,
+ "%s() - error = %s",
+ __func__,
+ oboe::convertToText(error)
+ );
+ // Try to open and start a new stream after a disconnect.
+ if (mParent->open() == Result::OK) {
+ mParent->start();
+ }
+}
diff --git a/samples/minimaloboe/src/main/cpp/SimpleNoiseMaker.h b/samples/minimaloboe/src/main/cpp/SimpleNoiseMaker.h
new file mode 100644
index 00000000..6376c0b2
--- /dev/null
+++ b/samples/minimaloboe/src/main/cpp/SimpleNoiseMaker.h
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2022 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 SIMPLE_NOISE_MAKER_H
+#define SIMPLE_NOISE_MAKER_H
+
+#include "oboe/Oboe.h"
+
+/**
+ * Play white noise using Oboe.
+ */
+class SimpleNoiseMaker {
+public:
+
+ /**
+ * Open an Oboe stream.
+ * @return OK or negative error code.
+ */
+ oboe::Result open();
+
+ oboe::Result start();
+
+ oboe::Result stop();
+
+ oboe::Result close();
+
+private:
+
+ class MyDataCallback : public oboe::AudioStreamDataCallback {
+ public:
+ oboe::DataCallbackResult onAudioReady(
+ oboe::AudioStream *audioStream,
+ void *audioData,
+ int32_t numFrames) override;
+
+ };
+
+ class MyErrorCallback : public oboe::AudioStreamErrorCallback {
+ public:
+ MyErrorCallback(SimpleNoiseMaker *parent) : mParent(parent) {}
+
+ virtual ~MyErrorCallback() {
+ }
+
+ void onErrorAfterClose(oboe::AudioStream *oboeStream, oboe::Result error) override;
+
+ private:
+ SimpleNoiseMaker *mParent;
+ };
+
+ std::shared_ptr<oboe::AudioStream> mStream;
+ std::shared_ptr<MyDataCallback> mDataCallback;
+ std::shared_ptr<MyErrorCallback> mErrorCallback;
+
+ static constexpr int kChannelCount = 2;
+};
+
+#endif //SIMPLE_NOISE_MAKER_H
diff --git a/samples/minimaloboe/src/main/java/com/example/minimaloboe/AudioPlayer.kt b/samples/minimaloboe/src/main/java/com/example/minimaloboe/AudioPlayer.kt
new file mode 100644
index 00000000..4871d741
--- /dev/null
+++ b/samples/minimaloboe/src/main/java/com/example/minimaloboe/AudioPlayer.kt
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2022 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.example.minimaloboe
+
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.LifecycleOwner
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.plus
+import kotlin.coroutines.CoroutineContext
+
+object AudioPlayer : DefaultLifecycleObserver {
+
+ // Create a coroutine scope which we can launch coroutines from. This way, if our
+ // player is ever destroyed (for example, if it was no longer a singleton and had multiple
+ // instances) any jobs would also be cancelled.
+ private val coroutineScope = CoroutineScope(Dispatchers.Default) + Job()
+
+ private var _playerState = MutableStateFlow<PlayerState>(PlayerState.NoResultYet)
+ val playerState = _playerState.asStateFlow()
+
+ init {
+ // Load the library containing the native code including the JNI functions.
+ System.loadLibrary("minimaloboe")
+ }
+
+ fun setPlaybackEnabled(isEnabled: Boolean) {
+ // Start (and stop) Oboe from a coroutine in case it blocks for too long.
+ // If the AudioServer has died it may take several seconds to recover.
+ // That can cause an ANR if we are starting audio from the main UI thread.
+ coroutineScope.launch {
+
+ val result = if (isEnabled) {
+ startAudioStreamNative()
+ } else {
+ stopAudioStreamNative()
+ }
+
+ val newUiState = if (result == 0) {
+ if (isEnabled){
+ PlayerState.Started
+ } else {
+ PlayerState.Stopped
+ }
+ } else {
+ PlayerState.Unknown(result)
+ }
+
+ _playerState.update { newUiState }
+ }
+ }
+
+ override fun onStop(owner: LifecycleOwner) {
+ super.onStop(owner)
+ setPlaybackEnabled(false)
+ }
+
+ private external fun startAudioStreamNative(): Int
+ private external fun stopAudioStreamNative(): Int
+}
+
+sealed interface PlayerState {
+ object NoResultYet : PlayerState
+ object Started : PlayerState
+ object Stopped : PlayerState
+ data class Unknown(val resultCode: Int) : PlayerState
+}
diff --git a/samples/minimaloboe/src/main/java/com/example/minimaloboe/MainActivity.kt b/samples/minimaloboe/src/main/java/com/example/minimaloboe/MainActivity.kt
new file mode 100644
index 00000000..edb29011
--- /dev/null
+++ b/samples/minimaloboe/src/main/java/com/example/minimaloboe/MainActivity.kt
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2022 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.example.minimaloboe
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material.Button
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Surface
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.lifecycle.ProcessLifecycleOwner
+import androidx.lifecycle.compose.ExperimentalLifecycleComposeApi
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.example.minimaloboe.ui.theme.SamplesTheme
+
+class MainActivity : ComponentActivity() {
+
+ @OptIn(ExperimentalLifecycleComposeApi::class)
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ // Let our AudioPlayer observe lifecycle events for the application so when it goes into the
+ // background we can stop audio playback.
+ ProcessLifecycleOwner.get().lifecycle.addObserver(AudioPlayer)
+
+ setContent {
+ SamplesTheme {
+ // A surface container using the 'background' color from the theme
+ Surface(
+ modifier = Modifier.fillMaxSize(),
+ color = MaterialTheme.colors.background
+ ) {
+ MainControls()
+ }
+ }
+ }
+ }
+}
+
+@ExperimentalLifecycleComposeApi
+@Composable
+fun MainControls() {
+ val playerState by AudioPlayer.playerState.collectAsStateWithLifecycle()
+ MainControls(playerState, AudioPlayer::setPlaybackEnabled)
+}
+
+@ExperimentalLifecycleComposeApi
+@Composable
+fun MainControls(playerState: PlayerState, setPlaybackEnabled: (Boolean) -> Unit) {
+
+ Column {
+
+ val isPlaying = playerState is PlayerState.Started
+
+ Text(text = "Minimal Oboe!")
+
+ Row {
+ Button(
+ onClick = { setPlaybackEnabled(true) },
+ enabled = !isPlaying
+ ) {
+ Text(text = "Start Audio")
+ }
+ Button(
+ onClick = { setPlaybackEnabled(false) },
+ enabled = isPlaying
+ ) {
+ Text(text = "Stop Audio")
+ }
+ }
+
+ // Create a status message for displaying the current playback state.
+ val uiStatusMessage = "Current status: " +
+ when (playerState) {
+ PlayerState.NoResultYet -> "No result yet"
+ PlayerState.Started -> "Started"
+ PlayerState.Stopped -> "Stopped"
+ is PlayerState.Unknown -> {
+ "Unknown. Result = " + playerState.resultCode
+ }
+ }
+
+ Text(uiStatusMessage)
+ }
+}
+
+@OptIn(ExperimentalLifecycleComposeApi::class)
+@Preview(showBackground = true)
+@Composable
+fun DefaultPreview() {
+ SamplesTheme {
+ MainControls(PlayerState.Started) { }
+ }
+}
diff --git a/samples/minimaloboe/src/main/java/com/example/minimaloboe/ui/theme/Color.kt b/samples/minimaloboe/src/main/java/com/example/minimaloboe/ui/theme/Color.kt
new file mode 100644
index 00000000..c4f8298b
--- /dev/null
+++ b/samples/minimaloboe/src/main/java/com/example/minimaloboe/ui/theme/Color.kt
@@ -0,0 +1,8 @@
+package com.example.minimaloboe.ui.theme
+
+import androidx.compose.ui.graphics.Color
+
+val Purple200 = Color(0xFFBB86FC)
+val Purple500 = Color(0xFF6200EE)
+val Purple700 = Color(0xFF3700B3)
+val Teal200 = Color(0xFF03DAC5)
diff --git a/samples/minimaloboe/src/main/java/com/example/minimaloboe/ui/theme/Shape.kt b/samples/minimaloboe/src/main/java/com/example/minimaloboe/ui/theme/Shape.kt
new file mode 100644
index 00000000..ca2815d9
--- /dev/null
+++ b/samples/minimaloboe/src/main/java/com/example/minimaloboe/ui/theme/Shape.kt
@@ -0,0 +1,11 @@
+package com.example.minimaloboe.ui.theme
+
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.Shapes
+import androidx.compose.ui.unit.dp
+
+val Shapes = Shapes(
+ small = RoundedCornerShape(4.dp),
+ medium = RoundedCornerShape(4.dp),
+ large = RoundedCornerShape(0.dp)
+)
diff --git a/samples/minimaloboe/src/main/java/com/example/minimaloboe/ui/theme/Theme.kt b/samples/minimaloboe/src/main/java/com/example/minimaloboe/ui/theme/Theme.kt
new file mode 100644
index 00000000..28c66a0e
--- /dev/null
+++ b/samples/minimaloboe/src/main/java/com/example/minimaloboe/ui/theme/Theme.kt
@@ -0,0 +1,44 @@
+package com.example.minimaloboe.ui.theme
+
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.darkColors
+import androidx.compose.material.lightColors
+import androidx.compose.runtime.Composable
+
+private val DarkColorPalette = darkColors(
+ primary = Purple200,
+ primaryVariant = Purple700,
+ secondary = Teal200
+)
+
+private val LightColorPalette = lightColors(
+ primary = Purple500,
+ primaryVariant = Purple700,
+ secondary = Teal200
+
+ /* Other default colors to override
+ background = Color.White,
+ surface = Color.White,
+ onPrimary = Color.White,
+ onSecondary = Color.Black,
+ onBackground = Color.Black,
+ onSurface = Color.Black,
+ */
+)
+
+@Composable
+fun SamplesTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) {
+ val colors = if (darkTheme) {
+ DarkColorPalette
+ } else {
+ LightColorPalette
+ }
+
+ MaterialTheme(
+ colors = colors,
+ typography = Typography,
+ shapes = Shapes,
+ content = content
+ )
+}
diff --git a/samples/minimaloboe/src/main/java/com/example/minimaloboe/ui/theme/Type.kt b/samples/minimaloboe/src/main/java/com/example/minimaloboe/ui/theme/Type.kt
new file mode 100644
index 00000000..b391730f
--- /dev/null
+++ b/samples/minimaloboe/src/main/java/com/example/minimaloboe/ui/theme/Type.kt
@@ -0,0 +1,21 @@
+package com.example.minimaloboe.ui.theme
+
+import androidx.compose.material.Typography
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
+
+// Set of Material typography styles to start with
+val Typography = Typography(
+ body1 = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 24.sp
+ ),
+ button = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.W500,
+ fontSize = 20.sp
+ )
+)
diff --git a/samples/minimaloboe/src/main/res/drawable-v24/ic_launcher_foreground.xml b/samples/minimaloboe/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 00000000..2b068d11
--- /dev/null
+++ b/samples/minimaloboe/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:aapt="http://schemas.android.com/aapt"
+ android:width="108dp"
+ android:height="108dp"
+ android:viewportWidth="108"
+ android:viewportHeight="108">
+ <path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
+ <aapt:attr name="android:fillColor">
+ <gradient
+ android:endX="85.84757"
+ android:endY="92.4963"
+ android:startX="42.9492"
+ android:startY="49.59793"
+ android:type="linear">
+ <item
+ android:color="#44000000"
+ android:offset="0.0" />
+ <item
+ android:color="#00000000"
+ android:offset="1.0" />
+ </gradient>
+ </aapt:attr>
+ </path>
+ <path
+ android:fillColor="#FFFFFF"
+ android:fillType="nonZero"
+ android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
+ android:strokeWidth="1"
+ android:strokeColor="#00000000" />
+</vector> \ No newline at end of file
diff --git a/samples/minimaloboe/src/main/res/drawable/ic_launcher_background.xml b/samples/minimaloboe/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 00000000..07d5da9c
--- /dev/null
+++ b/samples/minimaloboe/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="108dp"
+ android:height="108dp"
+ android:viewportWidth="108"
+ android:viewportHeight="108">
+ <path
+ android:fillColor="#3DDC84"
+ android:pathData="M0,0h108v108h-108z" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M9,0L9,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,0L19,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M29,0L29,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M39,0L39,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M49,0L49,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M59,0L59,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M69,0L69,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M79,0L79,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M89,0L89,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M99,0L99,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,9L108,9"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,19L108,19"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,29L108,29"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,39L108,39"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,49L108,49"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,59L108,59"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,69L108,69"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,79L108,79"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,89L108,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,99L108,99"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,29L89,29"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,39L89,39"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,49L89,49"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,59L89,59"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,69L89,69"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,79L89,79"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M29,19L29,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M39,19L39,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M49,19L49,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M59,19L59,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M69,19L69,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M79,19L79,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+</vector>
diff --git a/samples/minimaloboe/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/samples/minimaloboe/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 00000000..eca70cfe
--- /dev/null
+++ b/samples/minimaloboe/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="@drawable/ic_launcher_background" />
+ <foreground android:drawable="@drawable/ic_launcher_foreground" />
+</adaptive-icon> \ No newline at end of file
diff --git a/samples/minimaloboe/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/samples/minimaloboe/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 00000000..eca70cfe
--- /dev/null
+++ b/samples/minimaloboe/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="@drawable/ic_launcher_background" />
+ <foreground android:drawable="@drawable/ic_launcher_foreground" />
+</adaptive-icon> \ No newline at end of file
diff --git a/samples/minimaloboe/src/main/res/mipmap-hdpi/ic_launcher.webp b/samples/minimaloboe/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 00000000..c209e78e
--- /dev/null
+++ b/samples/minimaloboe/src/main/res/mipmap-hdpi/ic_launcher.webp
Binary files differ
diff --git a/samples/minimaloboe/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/samples/minimaloboe/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 00000000..b2dfe3d1
--- /dev/null
+++ b/samples/minimaloboe/src/main/res/mipmap-hdpi/ic_launcher_round.webp
Binary files differ
diff --git a/samples/minimaloboe/src/main/res/mipmap-mdpi/ic_launcher.webp b/samples/minimaloboe/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 00000000..4f0f1d64
--- /dev/null
+++ b/samples/minimaloboe/src/main/res/mipmap-mdpi/ic_launcher.webp
Binary files differ
diff --git a/samples/minimaloboe/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/samples/minimaloboe/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 00000000..62b611da
--- /dev/null
+++ b/samples/minimaloboe/src/main/res/mipmap-mdpi/ic_launcher_round.webp
Binary files differ
diff --git a/samples/minimaloboe/src/main/res/mipmap-xhdpi/ic_launcher.webp b/samples/minimaloboe/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 00000000..948a3070
--- /dev/null
+++ b/samples/minimaloboe/src/main/res/mipmap-xhdpi/ic_launcher.webp
Binary files differ
diff --git a/samples/minimaloboe/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/samples/minimaloboe/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 00000000..1b9a6956
--- /dev/null
+++ b/samples/minimaloboe/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
Binary files differ
diff --git a/samples/minimaloboe/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/samples/minimaloboe/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 00000000..28d4b77f
--- /dev/null
+++ b/samples/minimaloboe/src/main/res/mipmap-xxhdpi/ic_launcher.webp
Binary files differ
diff --git a/samples/minimaloboe/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/samples/minimaloboe/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 00000000..9287f508
--- /dev/null
+++ b/samples/minimaloboe/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
Binary files differ
diff --git a/samples/minimaloboe/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/samples/minimaloboe/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 00000000..aa7d6427
--- /dev/null
+++ b/samples/minimaloboe/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
Binary files differ
diff --git a/samples/minimaloboe/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/samples/minimaloboe/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 00000000..9126ae37
--- /dev/null
+++ b/samples/minimaloboe/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
Binary files differ
diff --git a/samples/minimaloboe/src/main/res/values/colors.xml b/samples/minimaloboe/src/main/res/values/colors.xml
new file mode 100644
index 00000000..f8c6127d
--- /dev/null
+++ b/samples/minimaloboe/src/main/res/values/colors.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <color name="purple_200">#FFBB86FC</color>
+ <color name="purple_500">#FF6200EE</color>
+ <color name="purple_700">#FF3700B3</color>
+ <color name="teal_200">#FF03DAC5</color>
+ <color name="teal_700">#FF018786</color>
+ <color name="black">#FF000000</color>
+ <color name="white">#FFFFFFFF</color>
+</resources> \ No newline at end of file
diff --git a/samples/minimaloboe/src/main/res/values/strings.xml b/samples/minimaloboe/src/main/res/values/strings.xml
new file mode 100644
index 00000000..67117ecb
--- /dev/null
+++ b/samples/minimaloboe/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+<resources>
+ <string name="app_name">MinimalOboe</string>
+</resources> \ No newline at end of file
diff --git a/samples/minimaloboe/src/main/res/values/themes.xml b/samples/minimaloboe/src/main/res/values/themes.xml
new file mode 100644
index 00000000..4a836ffe
--- /dev/null
+++ b/samples/minimaloboe/src/main/res/values/themes.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <style name="Theme.Samples" parent="android:Theme.Material.Light.NoActionBar">
+ <item name="android:statusBarColor">@color/purple_700</item>
+ </style>
+</resources> \ No newline at end of file
diff --git a/samples/settings.gradle b/samples/settings.gradle
index 88694a29..ddec58eb 100644
--- a/samples/settings.gradle
+++ b/samples/settings.gradle
@@ -24,3 +24,4 @@ include ':SoundBoard'
include ':drumthumper'
include ':parselib'
include ':iolib'
+include ':minimaloboe'