├── .clang-format ├── .gitignore ├── .idea ├── .gitignore ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── compiler.xml ├── gradle.xml ├── inspectionProfiles │ └── Project_Default.xml └── misc.xml ├── LICENSE.md ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── thewolfsound │ │ └── wavetablesynthesizer │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── cpp │ │ ├── CMakeLists.txt │ │ ├── OboeAudioPlayer.cpp │ │ ├── WavetableFactory.cpp │ │ ├── WavetableOscillator.cpp │ │ ├── WavetableSynthesizer.cpp │ │ ├── include │ │ │ ├── AudioPlayer.h │ │ │ ├── AudioSource.h │ │ │ ├── Log.h │ │ │ ├── MathConstants.h │ │ │ ├── OboeAudioPlayer.h │ │ │ ├── Wavetable.h │ │ │ ├── WavetableFactory.h │ │ │ ├── WavetableOscillator.h │ │ │ └── WavetableSynthesizer.h │ │ └── wavetablesynthesizer-native-lib.cpp │ ├── ic_launcher-playstore.png │ ├── java │ │ └── com │ │ │ └── thewolfsound │ │ │ └── wavetablesynthesizer │ │ │ ├── LoggingWavetableSynthesizer.kt │ │ │ ├── MainActivity.kt │ │ │ ├── NativeWavetableSynthesizer.kt │ │ │ ├── WavetableSynthesizer.kt │ │ │ ├── WavetableSynthesizerViewModel.kt │ │ │ └── ui │ │ │ └── theme │ │ │ ├── Color.kt │ │ │ ├── Shape.kt │ │ │ ├── Theme.kt │ │ │ └── Type.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_background.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_background.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_background.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_background.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_background.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ └── test │ └── java │ └── com │ └── thewolfsound │ └── wavetablesynthesizer │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | BasedOnStyle: Chromium 3 | 4 | ... 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 593 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 19 | 20 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 20 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Wavetable Synthesizer Android App 2 | 3 | ## Built using Jetpack Compose and the Oboe library 4 | 5 | By Jan Wilczek from TheWolfSound.com. 6 | 7 | This repository contains the source code of my tutorial on how to build a wavetable synthesizer on 8 | Android. 9 | 10 | [>>> Read the tutorial on TheWolfSound.com <<<](https://thewolfsound.com/android-synthesizer-1-app-architecture/) 11 | 12 | ## Project Goal 13 | 14 | The goal of the app is to build a wavetable synthesizer on Android with basic controls. You can see 15 | them in the user interface (UI) of the app. 16 | 17 | ![](https://thewolfsound.com/assets/img/posts/synthesis/android-wavetable-synthesizer/SynthesizerUI.webp) 18 | 19 | _Graphical user interface of the synthesizer app._ 20 | 21 | The secondary goal is to use cutting-edge Android tools and practices like 22 | 23 | * [Kotlin programming language](https://kotlinlang.org/), 24 | * [Jetpack Compose UI framework](https://developer.android.com/jetpack/compose), 25 | * [Oboe audio library](https://github.com/google/oboe), 26 | * and [modern Android architecture guidelines](https://developer.android.com/topic/architecture). 27 | 28 | Feel free to explore, comment, and give feedback! 29 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'org.jetbrains.kotlin.android' 4 | } 5 | 6 | android { 7 | signingConfigs { 8 | release { 9 | storeFile file('C:\\Users\\admin\\AndroidStudioProjects\\WavetableSynthesizer\\keystores\\WavetableSynthesizer.jks') 10 | } 11 | } 12 | compileSdk 31 13 | 14 | defaultConfig { 15 | applicationId "com.thewolfsound.wavetablesynthesizer" 16 | minSdk 30 17 | targetSdk 31 18 | versionCode 1 19 | versionName "1.0" 20 | 21 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 22 | vectorDrawables { 23 | useSupportLibrary true 24 | } 25 | externalNativeBuild { 26 | cmake { 27 | cppFlags '-std=c++2a' 28 | arguments '-DANDROID_STL=c++_shared' 29 | } 30 | } 31 | } 32 | 33 | buildTypes { 34 | release { 35 | minifyEnabled false 36 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 37 | } 38 | } 39 | compileOptions { 40 | sourceCompatibility JavaVersion.VERSION_1_8 41 | targetCompatibility JavaVersion.VERSION_1_8 42 | } 43 | kotlinOptions { 44 | jvmTarget = '1.8' 45 | freeCompilerArgs += '-Xjvm-default=compatibility' 46 | } 47 | buildFeatures { 48 | compose true 49 | prefab true 50 | } 51 | composeOptions { 52 | kotlinCompilerExtensionVersion compose_version 53 | } 54 | packagingOptions { 55 | resources { 56 | excludes += '/META-INF/{AL2.0,LGPL2.1}' 57 | } 58 | } 59 | externalNativeBuild { 60 | cmake { 61 | path file('src/main/cpp/CMakeLists.txt') 62 | version '3.18.1' 63 | } 64 | } 65 | } 66 | 67 | dependencies { 68 | 69 | implementation 'androidx.core:core-ktx:1.8.0' 70 | implementation "androidx.compose.ui:ui:$compose_version" 71 | implementation "androidx.compose.material:material:$compose_version" 72 | implementation "androidx.compose.material:material-icons-extended:$compose_version" 73 | implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" 74 | implementation "androidx.compose.runtime:runtime-livedata:$compose_version" 75 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.0' 76 | implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.5.0" 77 | implementation 'androidx.activity:activity-compose:1.5.0' 78 | testImplementation 'junit:junit:4.13.2' 79 | androidTestImplementation 'androidx.test.ext:junit:1.1.3' 80 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' 81 | androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version" 82 | debugImplementation "androidx.compose.ui:ui-tooling:$compose_version" 83 | implementation "com.google.oboe:oboe:1.6.1" 84 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/androidTest/java/com/thewolfsound/wavetablesynthesizer/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.thewolfsound.wavetablesynthesizer 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | 19 | @Test 20 | fun useAppContext() { 21 | // Context of the app under test. 22 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 23 | assertEquals("com.thewolfsound.wavetablesynthesizer", appContext.packageName) 24 | } 25 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 13 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/cpp/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | 2 | # For more information about using CMake with Android Studio, read the 3 | # documentation: https://d.android.com/studio/projects/add-native-code.html 4 | 5 | # Sets the minimum version of CMake required to build the native library. 6 | 7 | cmake_minimum_required(VERSION 3.18.1) 8 | 9 | # Declares and names the project. 10 | 11 | project("wavetablesynthesizer") 12 | 13 | # Creates and names a library, sets it as either STATIC 14 | # or SHARED, and provides the relative paths to its source code. 15 | # You can define multiple libraries, and CMake builds them for you. 16 | # Gradle automatically packages shared libraries with your APK. 17 | 18 | add_library( # Sets the name of the library. 19 | wavetablesynthesizer 20 | 21 | # Sets the library as a shared library. 22 | SHARED 23 | 24 | # Provides a relative path to your source file(s). 25 | wavetablesynthesizer-native-lib.cpp 26 | WavetableSynthesizer.cpp 27 | WavetableOscillator.cpp 28 | WavetableFactory.cpp 29 | OboeAudioPlayer.cpp 30 | ) 31 | 32 | include_directories( 33 | include 34 | ) 35 | 36 | # Searches for a specified prebuilt library and stores the path as a 37 | # variable. Because CMake includes system libraries in the search path by 38 | # default, you only need to specify the name of the public NDK library 39 | # you want to add. CMake verifies that the library exists before 40 | # completing its build. 41 | 42 | find_library( # Sets the name of the path variable. 43 | log-lib 44 | 45 | # Specifies the name of the NDK library that 46 | # you want CMake to locate. 47 | log 48 | ) 49 | find_package(oboe REQUIRED CONFIG) 50 | 51 | # Specifies libraries CMake should link to your target library. You 52 | # can link multiple libraries, such as libraries you define in this 53 | # build script, prebuilt third-party libraries, or system libraries. 54 | 55 | target_link_libraries( # Specifies the target library. 56 | wavetablesynthesizer 57 | 58 | # Links the target library to the log library 59 | # included in the NDK. 60 | ${log-lib} 61 | oboe::oboe 62 | ) 63 | -------------------------------------------------------------------------------- /app/src/main/cpp/OboeAudioPlayer.cpp: -------------------------------------------------------------------------------- 1 | #include "OboeAudioPlayer.h" 2 | 3 | #include 4 | #include "AudioSource.h" 5 | #include "Log.h" 6 | 7 | using namespace oboe; 8 | 9 | namespace wavetablesynthesizer { 10 | #ifndef NDEBUG 11 | static std::atomic instances{0}; 12 | #endif 13 | 14 | OboeAudioPlayer::OboeAudioPlayer(std::shared_ptr source, 15 | int samplingRate) 16 | : _source(std::move(source)), _samplingRate(samplingRate) { 17 | #ifndef NDEBUG 18 | LOGD("OboeAudioPlayer created. Instances count: %d", ++instances); 19 | #endif 20 | } 21 | 22 | OboeAudioPlayer::~OboeAudioPlayer() { 23 | #ifndef NDEBUG 24 | LOGD("OboeAudioPlayer destroyed. Instances count: %d", --instances); 25 | #endif 26 | OboeAudioPlayer::stop(); 27 | } 28 | 29 | int32_t OboeAudioPlayer::play() { 30 | LOGD("OboeAudioPlayer::play()"); 31 | AudioStreamBuilder builder; 32 | const auto result = 33 | builder.setPerformanceMode(PerformanceMode::LowLatency) 34 | ->setDirection(Direction::Output) 35 | ->setSampleRate(_samplingRate) 36 | ->setDataCallback(this) 37 | ->setSharingMode(SharingMode::Exclusive) 38 | ->setFormat(AudioFormat::Float) 39 | ->setChannelCount(channelCount) 40 | ->setSampleRateConversionQuality(SampleRateConversionQuality::Best) 41 | ->openStream(_stream); 42 | 43 | if (result != Result::OK) { 44 | return static_cast(result); 45 | } 46 | 47 | const auto playResult = _stream->requestStart(); 48 | 49 | return static_cast(playResult); 50 | } 51 | 52 | void OboeAudioPlayer::stop() { 53 | LOGD("OboeAudioPlayer::stop()"); 54 | 55 | if (_stream) { 56 | _stream->stop(); 57 | _stream->close(); 58 | _stream.reset(); 59 | } 60 | _source->onPlaybackStopped(); 61 | } 62 | 63 | DataCallbackResult OboeAudioPlayer::onAudioReady(oboe::AudioStream* audioStream, 64 | void* audioData, 65 | int32_t framesCount) { 66 | auto* floatData = reinterpret_cast(audioData); 67 | 68 | for (auto frame = 0; frame < framesCount; ++frame) { 69 | const auto sample = _source->getSample(); 70 | for (auto channel = 0; channel < channelCount; ++channel) { 71 | floatData[frame * channelCount + channel] = sample; 72 | } 73 | } 74 | return oboe::DataCallbackResult::Continue; 75 | } 76 | } // namespace wavetablesynthesizer 77 | -------------------------------------------------------------------------------- /app/src/main/cpp/WavetableFactory.cpp: -------------------------------------------------------------------------------- 1 | #include "WavetableFactory.h" 2 | #include 3 | #include 4 | #include "Wavetable.h" 5 | #include "MathConstants.h" 6 | 7 | namespace wavetablesynthesizer { 8 | namespace { 9 | constexpr auto WAVETABLE_LENGTH = 256; 10 | 11 | std::vector generateSineWaveTable() { 12 | auto sineWaveTable = std::vector(WAVETABLE_LENGTH); 13 | 14 | for (auto i = 0; i < WAVETABLE_LENGTH; ++i) { 15 | sineWaveTable[i] = 16 | std::sin(2 * PI * static_cast(i) / WAVETABLE_LENGTH); 17 | } 18 | 19 | return sineWaveTable; 20 | } 21 | 22 | std::vector generateTriangleWaveTable() { 23 | auto triangleWaveTable = std::vector(WAVETABLE_LENGTH, 0.f); 24 | 25 | constexpr auto HARMONICS_COUNT = 13; 26 | 27 | for (auto k = 1; k <= HARMONICS_COUNT; ++k) { 28 | for (auto j = 0; j < WAVETABLE_LENGTH; ++j) { 29 | const auto phase = 2.f * PI * 1.f * j / WAVETABLE_LENGTH; 30 | triangleWaveTable[j] += 8.f / std::pow(PI, 2.f) * std::pow(-1.f, k) * 31 | std::pow(2 * k - 1, -2.f) * 32 | std::sin((2.f * k - 1.f) * phase); 33 | } 34 | } 35 | 36 | return triangleWaveTable; 37 | } 38 | 39 | std::vector generateSquareWaveTable() { 40 | auto squareWaveTable = std::vector(WAVETABLE_LENGTH, 0.f); 41 | 42 | constexpr auto HARMONICS_COUNT = 7; 43 | 44 | for (auto k = 1; k <= HARMONICS_COUNT; ++k) { 45 | for (auto j = 0; j < WAVETABLE_LENGTH; ++j) { 46 | const auto phase = 2.f * PI * 1.f * j / WAVETABLE_LENGTH; 47 | squareWaveTable[j] += 4.f / PI * std::pow(2.f * k - 1.f, -1.f) * 48 | std::sin((2.f * k - 1.f) * phase); 49 | } 50 | } 51 | 52 | return squareWaveTable; 53 | } 54 | 55 | std::vector generateSawWaveTable() { 56 | auto sawWaveTable = std::vector(WAVETABLE_LENGTH, 0.f); 57 | 58 | constexpr auto HARMONICS_COUNT = 26; 59 | 60 | for (auto k = 1; k <= HARMONICS_COUNT; ++k) { 61 | for (auto j = 0; j < WAVETABLE_LENGTH; ++j) { 62 | const auto phase = 2.f * PI * 1.f * j / WAVETABLE_LENGTH; 63 | sawWaveTable[j] += 2.f / PI * std::pow(-1.f, k) * std::pow(k, -1.f) * 64 | std::sin(k * phase); 65 | } 66 | } 67 | 68 | return sawWaveTable; 69 | } 70 | 71 | template 72 | std::vector generateWaveTableOnce(std::vector& waveTable, 73 | F&& generator) { 74 | if (waveTable.empty()) { 75 | waveTable = generator(); 76 | } 77 | 78 | return waveTable; 79 | } 80 | } 81 | 82 | std::vector WavetableFactory::getWaveTable(Wavetable wavetable) { 83 | switch (wavetable) { 84 | case Wavetable::SINE: 85 | return sineWaveTable(); 86 | case Wavetable::TRIANGLE: 87 | return triangleWaveTable(); 88 | case Wavetable::SQUARE: 89 | return squareWaveTable(); 90 | case Wavetable::SAW: 91 | return sawWaveTable(); 92 | default: 93 | return std::vector(WAVETABLE_LENGTH, 0.f); 94 | } 95 | } 96 | 97 | std::vector WavetableFactory::sineWaveTable() { 98 | return generateWaveTableOnce(_sineWaveTable, &generateSineWaveTable); 99 | } 100 | 101 | std::vector WavetableFactory::triangleWaveTable() { 102 | return generateWaveTableOnce(_triangleWaveTable, &generateTriangleWaveTable); 103 | } 104 | 105 | std::vector WavetableFactory::squareWaveTable() { 106 | return generateWaveTableOnce(_squareWaveTable, &generateSquareWaveTable); 107 | } 108 | 109 | std::vector WavetableFactory::sawWaveTable() { 110 | return generateWaveTableOnce(_sawWaveTable, &generateSawWaveTable); 111 | } 112 | } // namespace wavetablesynthesizer -------------------------------------------------------------------------------- /app/src/main/cpp/WavetableOscillator.cpp: -------------------------------------------------------------------------------- 1 | #include "WavetableOscillator.h" 2 | #include 3 | #include "MathConstants.h" 4 | 5 | namespace wavetablesynthesizer { 6 | 7 | WavetableOscillator::WavetableOscillator(std::vector waveTable, 8 | float sampleRate) 9 | : waveTable{std::move(waveTable)}, sampleRate{sampleRate} {} 10 | 11 | float WavetableOscillator::getSample() { 12 | swapWavetableIfNecessary(); 13 | 14 | index = std::fmod(index, static_cast(waveTable.size())); 15 | const auto sample = interpolateLinearly(); 16 | index += indexIncrement; 17 | return amplitude * sample; 18 | } 19 | 20 | void WavetableOscillator::swapWavetableIfNecessary() { 21 | wavetableIsBeingSwapped.store(true, std::memory_order_release); 22 | if (swapWavetable.load(std::memory_order_acquire)) { 23 | std::swap(waveTable, wavetableToSwap); 24 | swapWavetable.store(false, std::memory_order_relaxed); 25 | } 26 | wavetableIsBeingSwapped.store(false, std::memory_order_release); 27 | } 28 | 29 | void WavetableOscillator::setFrequency(float frequency) { 30 | indexIncrement = frequency * static_cast(waveTable.size()) / 31 | static_cast(sampleRate); 32 | } 33 | 34 | void WavetableOscillator::onPlaybackStopped() { 35 | index = 0.f; 36 | } 37 | 38 | float WavetableOscillator::interpolateLinearly() const { 39 | const auto truncatedIndex = 40 | static_cast(index); 41 | const auto nextIndex = (truncatedIndex + 1u) % waveTable.size(); 42 | const auto nextIndexWeight = index - static_cast(truncatedIndex); 43 | return waveTable[nextIndex] * nextIndexWeight + 44 | (1.f - nextIndexWeight) * waveTable[truncatedIndex]; 45 | } 46 | 47 | void WavetableOscillator::setAmplitude(float newAmplitude) { 48 | amplitude.store(newAmplitude); 49 | } 50 | 51 | void WavetableOscillator::setWavetable(const std::vector &wavetable) { 52 | // Wait for the previous swap to take place if the oscillator is playing 53 | swapWavetable.store(false, std::memory_order_release); 54 | while (wavetableIsBeingSwapped.load(std::memory_order_acquire)) { 55 | } 56 | wavetableToSwap = wavetable; 57 | swapWavetable.store(true, std::memory_order_release); 58 | } 59 | 60 | A4Oscillator::A4Oscillator(float sampleRate) 61 | : _phaseIncrement{2.f * PI * 440.f / sampleRate} {} 62 | 63 | float A4Oscillator::getSample() { 64 | const auto sample = 0.5f * std::sin(_phase); 65 | _phase = std::fmod(_phase + _phaseIncrement, 2.f * PI); 66 | return sample; 67 | } 68 | 69 | void A4Oscillator::onPlaybackStopped() { 70 | _phase = 0.f; 71 | } 72 | } // namespace wavetablesynthesizer 73 | -------------------------------------------------------------------------------- /app/src/main/cpp/WavetableSynthesizer.cpp: -------------------------------------------------------------------------------- 1 | #include "WavetableSynthesizer.h" 2 | #include 3 | #include "Log.h" 4 | #include "OboeAudioPlayer.h" 5 | #include "WavetableOscillator.h" 6 | 7 | namespace wavetablesynthesizer { 8 | float dBToAmplitude(float dB) { 9 | return std::pow(10.f, dB / 20.f); 10 | } 11 | 12 | WavetableSynthesizer::WavetableSynthesizer() 13 | : _oscillator{std::make_shared(_wavetableFactory.getWaveTable(_currentWavetable), samplingRate)}, 14 | _audioPlayer{ 15 | std::make_unique(_oscillator, samplingRate)} {} 16 | 17 | WavetableSynthesizer::~WavetableSynthesizer() = default; 18 | 19 | bool WavetableSynthesizer::isPlaying() const { 20 | LOGD("isPlaying() called"); 21 | return _isPlaying; 22 | } 23 | 24 | void WavetableSynthesizer::play() { 25 | LOGD("play() called"); 26 | std::lock_guard lock(_mutex); 27 | const auto result = _audioPlayer->play(); 28 | if (result == 0) { 29 | _isPlaying = true; 30 | } else { 31 | LOGD("Could not start playback."); 32 | } 33 | } 34 | 35 | void WavetableSynthesizer::setFrequency(float frequencyInHz) { 36 | LOGD("Frequency set to %.2f Hz.", frequencyInHz); 37 | _oscillator->setFrequency(frequencyInHz); 38 | } 39 | 40 | void WavetableSynthesizer::setVolume(float volumeInDb) { 41 | LOGD("Volume set to %.2f dB.", volumeInDb); 42 | const auto amplitude = dBToAmplitude(volumeInDb); 43 | _oscillator->setAmplitude(amplitude); 44 | } 45 | 46 | void WavetableSynthesizer::setWavetable(Wavetable wavetable) { 47 | if (_currentWavetable != wavetable) { 48 | _currentWavetable = wavetable; 49 | _oscillator->setWavetable(_wavetableFactory.getWaveTable(wavetable)); 50 | } 51 | } 52 | 53 | void WavetableSynthesizer::stop() { 54 | LOGD("stop() called"); 55 | std::lock_guard lock(_mutex); 56 | _audioPlayer->stop(); 57 | _isPlaying = false; 58 | } 59 | } // namespace wavetablesynthesizer 60 | -------------------------------------------------------------------------------- /app/src/main/cpp/include/AudioPlayer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace wavetablesynthesizer { 4 | class AudioPlayer { 5 | public: 6 | virtual ~AudioPlayer() = default; 7 | 8 | virtual int32_t play() = 0; 9 | 10 | virtual void stop() = 0; 11 | }; 12 | } // namespace wavetablesynthesizer 13 | -------------------------------------------------------------------------------- /app/src/main/cpp/include/AudioSource.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace wavetablesynthesizer { 4 | class AudioSource { 5 | public: 6 | virtual ~AudioSource() = default; 7 | 8 | virtual float getSample() = 0; 9 | 10 | virtual void onPlaybackStopped() = 0; 11 | }; 12 | } // namespace wavetablesynthesizer 13 | -------------------------------------------------------------------------------- /app/src/main/cpp/include/Log.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #ifndef NDEBUG 6 | #define LOGD(args...) \ 7 | __android_log_print(android_LogPriority::ANDROID_LOG_DEBUG, "WavetableSynthesizer", args) 8 | #else 9 | #define LOGD(args...) 10 | #endif 11 | -------------------------------------------------------------------------------- /app/src/main/cpp/include/MathConstants.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace wavetablesynthesizer { 4 | static const auto PI = std::atan(1.f) * 4; 5 | } 6 | -------------------------------------------------------------------------------- /app/src/main/cpp/include/OboeAudioPlayer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "AudioPlayer.h" 5 | 6 | namespace wavetablesynthesizer { 7 | class AudioSource; 8 | 9 | class OboeAudioPlayer : public oboe::AudioStreamDataCallback, 10 | public AudioPlayer { 11 | public: 12 | static constexpr auto channelCount = oboe::ChannelCount::Mono; 13 | 14 | OboeAudioPlayer(std::shared_ptr source, int samplingRate); 15 | ~OboeAudioPlayer(); 16 | 17 | int32_t play() override; 18 | 19 | void stop() override; 20 | 21 | oboe::DataCallbackResult onAudioReady(oboe::AudioStream* audioStream, 22 | void* audioData, 23 | int32_t framesCount) override; 24 | 25 | private: 26 | std::shared_ptr _source; 27 | std::shared_ptr _stream; 28 | int _samplingRate; 29 | }; 30 | } // namespace wavetablesynthesizer 31 | -------------------------------------------------------------------------------- /app/src/main/cpp/include/Wavetable.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace wavetablesynthesizer { 4 | enum class Wavetable { SINE, TRIANGLE, SQUARE, SAW }; 5 | } 6 | -------------------------------------------------------------------------------- /app/src/main/cpp/include/WavetableFactory.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | namespace wavetablesynthesizer { 5 | enum class Wavetable; 6 | 7 | class WavetableFactory { 8 | public: 9 | std::vector getWaveTable(Wavetable wavetable); 10 | 11 | private: 12 | std::vector sineWaveTable(); 13 | std::vector triangleWaveTable(); 14 | std::vector squareWaveTable(); 15 | std::vector sawWaveTable(); 16 | 17 | std::vector _sineWaveTable; 18 | std::vector _triangleWaveTable; 19 | std::vector _squareWaveTable; 20 | std::vector _sawWaveTable; 21 | }; 22 | } // namespace wavetablesynthesizer 23 | -------------------------------------------------------------------------------- /app/src/main/cpp/include/WavetableOscillator.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "AudioSource.h" 5 | 6 | namespace wavetablesynthesizer { 7 | 8 | class WavetableOscillator : public AudioSource { 9 | public: 10 | WavetableOscillator() = default; 11 | WavetableOscillator(std::vector waveTable, float sampleRate); 12 | 13 | float getSample() override; 14 | 15 | virtual void setFrequency(float frequency); 16 | 17 | virtual void setAmplitude(float newAmplitude); 18 | 19 | void onPlaybackStopped() override; 20 | 21 | virtual void setWavetable(const std::vector &wavetable); 22 | 23 | private: 24 | float interpolateLinearly() const; 25 | void swapWavetableIfNecessary(); 26 | 27 | float index = 0.f; 28 | std::atomic indexIncrement{0.f}; 29 | std::vector waveTable; 30 | float sampleRate; 31 | std::atomic amplitude{1.f}; 32 | 33 | std::atomic swapWavetable{false}; 34 | std::vector wavetableToSwap; 35 | std::atomic wavetableIsBeingSwapped{false}; 36 | }; 37 | 38 | class A4Oscillator : public WavetableOscillator { 39 | public: 40 | explicit A4Oscillator(float sampleRate); 41 | 42 | float getSample() override; 43 | 44 | void setFrequency(float frequency) override {}; 45 | 46 | void setAmplitude(float newAmplitude) override {}; 47 | 48 | void onPlaybackStopped() override; 49 | 50 | void setWavetable(const std::vector &wavetable) override {}; 51 | 52 | private: 53 | float _phase{0.f}; 54 | float _phaseIncrement{0.f}; 55 | }; 56 | } // namespace wavetablesynthesizer 57 | -------------------------------------------------------------------------------- /app/src/main/cpp/include/WavetableSynthesizer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include "Wavetable.h" 6 | #include "WavetableFactory.h" 7 | 8 | namespace wavetablesynthesizer { 9 | class WavetableOscillator; 10 | 11 | class AudioPlayer; 12 | 13 | constexpr auto samplingRate = 48000; 14 | 15 | class WavetableSynthesizer { 16 | public: 17 | WavetableSynthesizer(); 18 | 19 | ~WavetableSynthesizer(); 20 | 21 | void play(); 22 | 23 | void stop(); 24 | 25 | bool isPlaying() const; 26 | 27 | void setFrequency(float frequencyInHz); 28 | 29 | void setVolume(float volumeInDb); 30 | 31 | void setWavetable(Wavetable wavetable); 32 | 33 | private: 34 | std::atomic _isPlaying{false}; 35 | std::mutex _mutex; 36 | WavetableFactory _wavetableFactory; 37 | Wavetable _currentWavetable{Wavetable::SINE}; 38 | std::shared_ptr _oscillator; 39 | std::unique_ptr _audioPlayer; 40 | }; 41 | } // namespace wavetablesynthesizer 42 | -------------------------------------------------------------------------------- /app/src/main/cpp/wavetablesynthesizer-native-lib.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "Log.h" 4 | #include "WavetableSynthesizer.h" 5 | 6 | extern "C" { 7 | JNIEXPORT jlong JNICALL 8 | Java_com_thewolfsound_wavetablesynthesizer_NativeWavetableSynthesizer_create( 9 | JNIEnv* env, 10 | jobject obj) { 11 | auto synthesizer = 12 | std::make_unique(); 13 | 14 | if (not synthesizer) { 15 | LOGD("Failed to create the synthesizer."); 16 | synthesizer.reset(nullptr); 17 | } 18 | 19 | return reinterpret_cast(synthesizer.release()); 20 | } 21 | 22 | JNIEXPORT void JNICALL 23 | Java_com_thewolfsound_wavetablesynthesizer_NativeWavetableSynthesizer_delete( 24 | JNIEnv* env, 25 | jobject obj, 26 | jlong synthesizerHandle) { 27 | auto* synthesizer = 28 | reinterpret_cast( 29 | synthesizerHandle); 30 | 31 | if (not synthesizer) { 32 | LOGD("Attempt to destroy an unitialized synthesizer."); 33 | return; 34 | } 35 | 36 | delete synthesizer; 37 | } 38 | 39 | JNIEXPORT void JNICALL 40 | Java_com_thewolfsound_wavetablesynthesizer_NativeWavetableSynthesizer_play( 41 | JNIEnv* env, 42 | jobject obj, 43 | jlong synthesizerHandle) { 44 | auto* synthesizer = 45 | reinterpret_cast( 46 | synthesizerHandle); 47 | 48 | if (synthesizer) { 49 | synthesizer->play(); 50 | } else { 51 | LOGD( 52 | "Synthesizer not created. Please, create the synthesizer first by " 53 | "calling create()."); 54 | } 55 | } 56 | 57 | JNIEXPORT void JNICALL 58 | Java_com_thewolfsound_wavetablesynthesizer_NativeWavetableSynthesizer_stop( 59 | JNIEnv* env, 60 | jobject obj, 61 | jlong synthesizerHandle) { 62 | auto* synthesizer = 63 | reinterpret_cast( 64 | synthesizerHandle); 65 | 66 | if (synthesizer) { 67 | synthesizer->stop(); 68 | } else { 69 | LOGD( 70 | "Synthesizer not created. Please, create the synthesizer first by " 71 | "calling create()."); 72 | } 73 | } 74 | 75 | JNIEXPORT jboolean JNICALL 76 | Java_com_thewolfsound_wavetablesynthesizer_NativeWavetableSynthesizer_isPlaying( 77 | JNIEnv* env, 78 | jobject obj, 79 | jlong synthesizerHandle) { 80 | auto* synthesizer = 81 | reinterpret_cast( 82 | synthesizerHandle); 83 | 84 | if (not synthesizer) { 85 | LOGD( 86 | "Synthesizer not created. Please, create the synthesizer first by " 87 | "calling create()."); 88 | return false; 89 | } 90 | 91 | return synthesizer->isPlaying(); 92 | } 93 | 94 | JNIEXPORT void JNICALL 95 | Java_com_thewolfsound_wavetablesynthesizer_NativeWavetableSynthesizer_setFrequency( 96 | JNIEnv* env, 97 | jobject obj, 98 | jlong synthesizerHandle, 99 | jfloat frequencyInHz) { 100 | auto* synthesizer = 101 | reinterpret_cast( 102 | synthesizerHandle); 103 | const auto nativeFrequency = static_cast(frequencyInHz); 104 | 105 | if (synthesizer) { 106 | synthesizer->setFrequency(nativeFrequency); 107 | } else { 108 | LOGD( 109 | "Synthesizer not created. Please, create the synthesizer first by " 110 | "calling create()."); 111 | } 112 | } 113 | 114 | JNIEXPORT void JNICALL 115 | Java_com_thewolfsound_wavetablesynthesizer_NativeWavetableSynthesizer_setVolume( 116 | JNIEnv* env, 117 | jobject obj, 118 | jlong synthesizerHandle, 119 | jfloat volumeInDb) { 120 | auto* synthesizer = 121 | reinterpret_cast( 122 | synthesizerHandle); 123 | const auto nativeVolume = static_cast(volumeInDb); 124 | 125 | if (synthesizer) { 126 | synthesizer->setVolume(nativeVolume); 127 | } else { 128 | LOGD( 129 | "Synthesizer not created. Please, create the synthesizer first by " 130 | "calling create()."); 131 | } 132 | } 133 | 134 | JNIEXPORT void JNICALL 135 | Java_com_thewolfsound_wavetablesynthesizer_NativeWavetableSynthesizer_setWavetable( 136 | JNIEnv* env, 137 | jobject obj, 138 | jlong synthesizerHandle, 139 | jint wavetable) { 140 | auto* synthesizer = 141 | reinterpret_cast( 142 | synthesizerHandle); 143 | const auto nativeWavetable = static_cast(wavetable); 144 | 145 | if (synthesizer) { 146 | synthesizer->setWavetable(nativeWavetable); 147 | } else { 148 | LOGD( 149 | "Synthesizer not created. Please, create the synthesizer first by " 150 | "calling create()."); 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JanWilczek/android-wavetable-synthesizer/f45ad135fb2def969b4f1726aae8751d548394e1/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/com/thewolfsound/wavetablesynthesizer/LoggingWavetableSynthesizer.kt: -------------------------------------------------------------------------------- 1 | package com.thewolfsound.wavetablesynthesizer 2 | 3 | import android.util.Log 4 | 5 | class LoggingWavetableSynthesizer : WavetableSynthesizer { 6 | 7 | private var isPlaying = false 8 | 9 | override suspend fun play() { 10 | Log.d("LoggingWavetableSynthesizer", "play() called.") 11 | isPlaying = true 12 | } 13 | 14 | override suspend fun stop() { 15 | Log.d("LoggingWavetableSynthesizer", "stop() called.") 16 | isPlaying = false 17 | } 18 | 19 | override suspend fun isPlaying(): Boolean { 20 | return isPlaying 21 | } 22 | 23 | override suspend fun setFrequency(frequencyInHz: Float) { 24 | Log.d("LoggingWavetableSynthesizer", "Frequency set to $frequencyInHz Hz.") 25 | } 26 | 27 | override suspend fun setVolume(volumeInDb: Float) { 28 | Log.d("LoggingWavetableSynthesizer", "Volume set to $volumeInDb dB.") 29 | } 30 | 31 | override suspend fun setWavetable(wavetable: Wavetable) { 32 | Log.d("LoggingWavetableSynthesizer", "Wavetable set to $wavetable") 33 | } 34 | } -------------------------------------------------------------------------------- /app/src/main/java/com/thewolfsound/wavetablesynthesizer/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.thewolfsound.wavetablesynthesizer 2 | 3 | import android.content.pm.ActivityInfo 4 | import android.os.Bundle 5 | import androidx.activity.ComponentActivity 6 | import androidx.activity.compose.setContent 7 | import androidx.activity.viewModels 8 | import androidx.compose.foundation.layout.* 9 | import androidx.compose.material.* 10 | import androidx.compose.material.icons.Icons 11 | import androidx.compose.material.icons.filled.VolumeMute 12 | import androidx.compose.material.icons.filled.VolumeUp 13 | import androidx.compose.runtime.* 14 | import androidx.compose.runtime.livedata.observeAsState 15 | import androidx.compose.runtime.saveable.rememberSaveable 16 | import androidx.compose.ui.Alignment 17 | import androidx.compose.ui.Modifier 18 | import androidx.compose.ui.draw.rotate 19 | import androidx.compose.ui.platform.LocalConfiguration 20 | import androidx.compose.ui.res.stringResource 21 | import androidx.compose.ui.tooling.preview.Devices 22 | import androidx.compose.ui.tooling.preview.Preview 23 | import androidx.compose.ui.unit.dp 24 | import androidx.lifecycle.viewmodel.compose.viewModel 25 | import com.thewolfsound.wavetablesynthesizer.ui.theme.WavetableSynthesizerTheme 26 | 27 | 28 | class MainActivity : ComponentActivity() { 29 | 30 | private val synthesizer = NativeWavetableSynthesizer() 31 | private val synthesizerViewModel: WavetableSynthesizerViewModel by viewModels() 32 | 33 | override fun onCreate(savedInstanceState: Bundle?) { 34 | super.onCreate(savedInstanceState) 35 | requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE 36 | lifecycle.addObserver(synthesizer) 37 | // pass the synthesizer to the ViewModel 38 | synthesizerViewModel.wavetableSynthesizer = synthesizer 39 | setContent { 40 | WavetableSynthesizerTheme { 41 | Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) { 42 | // pass the ViewModel down the composables' hierarchy 43 | WavetableSynthesizerApp(Modifier, synthesizerViewModel) 44 | } 45 | } 46 | } 47 | } 48 | 49 | override fun onDestroy() { 50 | super.onDestroy() 51 | lifecycle.removeObserver(synthesizer) 52 | } 53 | 54 | override fun onResume() { 55 | super.onResume() 56 | synthesizerViewModel.applyParameters() 57 | } 58 | } 59 | 60 | @Composable 61 | fun WavetableSynthesizerApp( 62 | modifier: Modifier, 63 | synthesizerViewModel: WavetableSynthesizerViewModel = viewModel() 64 | ) { 65 | Column( 66 | modifier = modifier.fillMaxSize(), 67 | horizontalAlignment = Alignment.CenterHorizontally, 68 | verticalArrangement = Arrangement.Top, 69 | ) { 70 | WavetableSelectionPanel(modifier, synthesizerViewModel) 71 | ControlsPanel(modifier, synthesizerViewModel) 72 | } 73 | } 74 | 75 | @Composable 76 | private fun ControlsPanel( 77 | modifier: Modifier, 78 | synthesizerViewModel: WavetableSynthesizerViewModel 79 | ) { 80 | Row( 81 | modifier = modifier 82 | .fillMaxWidth() 83 | .fillMaxHeight(), 84 | horizontalArrangement = Arrangement.Center, 85 | verticalAlignment = Alignment.CenterVertically 86 | ) { 87 | Column( 88 | modifier = modifier.fillMaxWidth(0.7f), 89 | horizontalAlignment = Alignment.CenterHorizontally 90 | ) { 91 | PitchControl(modifier, synthesizerViewModel) 92 | PlayControl(modifier, synthesizerViewModel) 93 | } 94 | Column( 95 | verticalArrangement = Arrangement.Center, 96 | horizontalAlignment = Alignment.CenterHorizontally, 97 | modifier = modifier 98 | .fillMaxWidth() 99 | .fillMaxHeight() 100 | ) { 101 | VolumeControl(modifier, synthesizerViewModel) 102 | } 103 | } 104 | } 105 | 106 | @Composable 107 | private fun PlayControl(modifier: Modifier, synthesizerViewModel: WavetableSynthesizerViewModel) { 108 | // The label of the play button is now an observable state, an instance of State. 109 | // State is used because the label is the id value of the resource string. 110 | // Thanks to the fact that the composable observes the label, 111 | // the composable will be recomposed (redrawn) when the observed state changes. 112 | val playButtonLabel = synthesizerViewModel.playButtonLabel.observeAsState() 113 | 114 | PlayControlContent(modifier = modifier, 115 | // onClick handler now simply notifies the ViewModel that it has been clicked 116 | onClick = { 117 | synthesizerViewModel.playClicked() 118 | }, 119 | // playButtonLabel will never be null; if it is, then we have a serious implementation issue 120 | buttonLabel = stringResource(playButtonLabel.value!!)) 121 | } 122 | 123 | @Composable 124 | private fun PlayControlContent(modifier: Modifier, onClick: () -> Unit, buttonLabel: String) { 125 | Button(modifier = modifier, 126 | onClick = onClick) { 127 | Text(buttonLabel) 128 | } 129 | } 130 | 131 | @Composable 132 | private fun PitchControl( 133 | modifier: Modifier, 134 | synthesizerViewModel: WavetableSynthesizerViewModel 135 | ) { 136 | // if the frequency changes, recompose this composable 137 | val frequency = synthesizerViewModel.frequency.observeAsState() 138 | // the slider position state is hoisted by this composable; no need to embed it into 139 | // the ViewModel, which ideally, shouldn't be aware of the UI. 140 | // When the slider position changes, this composable will be recomposed as we explained in 141 | // the UI tutorial. 142 | val sliderPosition = rememberSaveable { 143 | mutableStateOf( 144 | // we use the ViewModel's convenience function to get the initial slider position 145 | synthesizerViewModel.sliderPositionFromFrequencyInHz(frequency.value!!) 146 | ) 147 | } 148 | 149 | PitchControlContent( 150 | modifier = modifier, 151 | pitchControlLabel = stringResource(R.string.frequency), 152 | value = sliderPosition.value, 153 | // on slider position change, update the slider position and the ViewModel 154 | onValueChange = { 155 | sliderPosition.value = it 156 | synthesizerViewModel.setFrequencySliderPosition(it) 157 | }, 158 | // this range is now [0, 1] because the ViewModel is responsible for calculating the frequency 159 | // out of the slider position 160 | valueRange = 0F..1F, 161 | // this label could be moved into the ViewModel but it doesn't have to be because this 162 | // composable will anyway be recomposed on a frequency change 163 | frequencyValueLabel = stringResource(R.string.frequency_value, frequency.value!!) 164 | ) 165 | } 166 | 167 | @Composable 168 | private fun PitchControlContent( 169 | modifier: Modifier, 170 | pitchControlLabel: String, 171 | value: Float, 172 | onValueChange: (Float) -> Unit, 173 | valueRange: ClosedFloatingPointRange, 174 | frequencyValueLabel: String 175 | ) { 176 | Text(pitchControlLabel, modifier = modifier) 177 | Slider(modifier = modifier, value = value, onValueChange = onValueChange, valueRange = valueRange) 178 | Row( 179 | modifier = modifier, 180 | horizontalArrangement = Arrangement.Center 181 | ) { 182 | Text(modifier = modifier, text = frequencyValueLabel) 183 | } 184 | } 185 | 186 | @Composable 187 | private fun VolumeControl(modifier: Modifier, synthesizerViewModel: WavetableSynthesizerViewModel) { 188 | // volume value is now an observable state; that means that the composable will be 189 | // recomposed (redrawn) when the observed state changes. 190 | val volume = synthesizerViewModel.volume.observeAsState() 191 | 192 | VolumeControlContent( 193 | modifier = modifier, 194 | // volume value should never be null; if it is, there's a serious implementation issue 195 | volume = volume.value!!, 196 | // use the value range from the ViewModel 197 | volumeRange = synthesizerViewModel.volumeRange, 198 | // on volume slider change, just update the ViewModel 199 | onValueChange = { synthesizerViewModel.setVolume(it) }) 200 | } 201 | 202 | @Composable 203 | private fun VolumeControlContent( 204 | modifier: Modifier, 205 | volume: Float, 206 | volumeRange: ClosedFloatingPointRange, 207 | onValueChange: (Float) -> Unit 208 | ) { 209 | // The volume slider should take around 1/4 of the screen height 210 | val screenHeight = LocalConfiguration.current.screenHeightDp 211 | val sliderHeight = screenHeight / 4 212 | 213 | Icon(imageVector = Icons.Filled.VolumeUp, contentDescription = null) 214 | Column( 215 | modifier = modifier 216 | .fillMaxWidth() 217 | .fillMaxHeight(0.8f) 218 | .offset(y = 40.dp), 219 | horizontalAlignment = Alignment.CenterHorizontally, 220 | verticalArrangement = Arrangement.SpaceBetween 221 | ) 222 | { 223 | Slider( 224 | value = volume, 225 | onValueChange = onValueChange, 226 | modifier = modifier 227 | .width(sliderHeight.dp) 228 | .rotate(270f), 229 | valueRange = volumeRange 230 | ) 231 | } 232 | Icon(imageVector = Icons.Filled.VolumeMute, contentDescription = null) 233 | } 234 | 235 | @Composable 236 | private fun WavetableSelectionPanel( 237 | modifier: Modifier, 238 | synthesizerViewModel: WavetableSynthesizerViewModel 239 | ) { 240 | Row( 241 | modifier = modifier 242 | .fillMaxWidth() 243 | .fillMaxHeight(0.5f), 244 | horizontalArrangement = Arrangement.SpaceEvenly, 245 | verticalAlignment = Alignment.CenterVertically 246 | ) { 247 | Column( 248 | modifier = modifier 249 | .fillMaxWidth() 250 | .fillMaxHeight(), 251 | verticalArrangement = Arrangement.SpaceEvenly, 252 | horizontalAlignment = Alignment.CenterHorizontally 253 | ) { 254 | Text(stringResource(R.string.wavetable)) 255 | WavetableSelectionButtons(modifier, synthesizerViewModel) 256 | } 257 | } 258 | } 259 | 260 | @Composable 261 | private fun WavetableSelectionButtons( 262 | modifier: Modifier, 263 | synthesizerViewModel: WavetableSynthesizerViewModel 264 | ) { 265 | Row( 266 | modifier = modifier.fillMaxWidth(), 267 | horizontalArrangement = Arrangement.SpaceEvenly 268 | ) { 269 | for (wavetable in Wavetable.values()) { 270 | WavetableButton( 271 | modifier = modifier, 272 | // update the ViewModel when the given wavetable is clicked 273 | onClick = { 274 | synthesizerViewModel.setWavetable(wavetable) 275 | }, 276 | // set the label to the resource string that corresponds to the wavetable 277 | label = stringResource(wavetable.toResourceString()), 278 | ) 279 | } 280 | } 281 | } 282 | 283 | @Composable 284 | private fun WavetableButton( 285 | modifier: Modifier, 286 | onClick: () -> Unit, 287 | label: String, 288 | ) { 289 | Button(modifier = modifier, onClick = onClick) { 290 | Text(label) 291 | } 292 | } 293 | 294 | @Preview(showBackground = true, device = Devices.AUTOMOTIVE_1024p, widthDp = 1024, heightDp = 720) 295 | @Composable 296 | fun WavetableSynthesizerPreview() { 297 | WavetableSynthesizerTheme { 298 | WavetableSynthesizerApp(Modifier, WavetableSynthesizerViewModel()) 299 | } 300 | } 301 | 302 | @Preview(showBackground = true, widthDp = 100, heightDp = 200) 303 | @Composable 304 | fun VolumeControlPreview() { 305 | Column( 306 | verticalArrangement = Arrangement.Top, 307 | horizontalAlignment = Alignment.CenterHorizontally, 308 | modifier = Modifier 309 | .fillMaxWidth() 310 | .fillMaxHeight() 311 | ) { 312 | VolumeControl(modifier = Modifier, synthesizerViewModel = WavetableSynthesizerViewModel()) 313 | } 314 | } -------------------------------------------------------------------------------- /app/src/main/java/com/thewolfsound/wavetablesynthesizer/NativeWavetableSynthesizer.kt: -------------------------------------------------------------------------------- 1 | package com.thewolfsound.wavetablesynthesizer 2 | 3 | import android.util.Log 4 | import androidx.lifecycle.* 5 | import kotlinx.coroutines.Dispatchers 6 | import kotlinx.coroutines.withContext 7 | 8 | class NativeWavetableSynthesizer : WavetableSynthesizer, DefaultLifecycleObserver { 9 | 10 | private var synthesizerHandle: Long = 0 11 | private val synthesizerMutex = Object() 12 | private external fun create(): Long 13 | private external fun delete(synthesizerHandle: Long) 14 | private external fun play(synthesizerHandle: Long) 15 | private external fun stop(synthesizerHandle: Long) 16 | private external fun isPlaying(synthesizerHandle: Long): Boolean 17 | private external fun setFrequency(synthesizerHandle: Long, frequencyInHz: Float) 18 | private external fun setVolume(synthesizerHandle: Long, amplitudeInDb: Float) 19 | private external fun setWavetable(synthesizerHandle: Long, wavetable: Int) 20 | 21 | companion object { 22 | init { 23 | System.loadLibrary("wavetablesynthesizer") 24 | } 25 | } 26 | 27 | override fun onResume(owner: LifecycleOwner) { 28 | super.onResume(owner) 29 | 30 | synchronized(synthesizerMutex) { 31 | Log.d("NativeWavetableSynthesizer", "onResume() called") 32 | createNativeHandleIfNotExists() 33 | } 34 | } 35 | 36 | override fun onPause(owner: LifecycleOwner) { 37 | super.onPause(owner) 38 | 39 | synchronized(synthesizerMutex) { 40 | Log.d("NativeWavetableSynthesizer", "onPause() called") 41 | 42 | if (synthesizerHandle == 0L) { 43 | Log.e("NativeWavetableSynthesizer", "Attempting to destroy a null synthesizer.") 44 | return 45 | } 46 | 47 | // Destroy the synthesizer 48 | delete(synthesizerHandle) 49 | synthesizerHandle = 0L 50 | } 51 | } 52 | 53 | override suspend fun play() = withContext(Dispatchers.Default) { 54 | synchronized(synthesizerMutex) { 55 | createNativeHandleIfNotExists() 56 | play(synthesizerHandle) 57 | } 58 | } 59 | 60 | override suspend fun stop() = withContext(Dispatchers.Default) { 61 | synchronized(synthesizerMutex) { 62 | createNativeHandleIfNotExists() 63 | stop(synthesizerHandle) 64 | } 65 | } 66 | 67 | override suspend fun isPlaying(): Boolean = withContext(Dispatchers.Default) { 68 | synchronized(synthesizerMutex) { 69 | createNativeHandleIfNotExists() 70 | return@withContext isPlaying(synthesizerHandle) 71 | } 72 | } 73 | 74 | override suspend fun setFrequency(frequencyInHz: Float) = withContext(Dispatchers.Default) { 75 | synchronized(synthesizerMutex) { 76 | createNativeHandleIfNotExists() 77 | setFrequency(synthesizerHandle, frequencyInHz) 78 | } 79 | } 80 | 81 | override suspend fun setVolume(volumeInDb: Float) = withContext(Dispatchers.Default) { 82 | synchronized(synthesizerMutex) { 83 | createNativeHandleIfNotExists() 84 | setVolume(synthesizerHandle, volumeInDb) 85 | } 86 | } 87 | 88 | override suspend fun setWavetable(wavetable: Wavetable) = withContext(Dispatchers.Default) { 89 | synchronized(synthesizerMutex) { 90 | createNativeHandleIfNotExists() 91 | setWavetable(synthesizerHandle, wavetable.ordinal) 92 | } 93 | } 94 | 95 | private fun createNativeHandleIfNotExists() { 96 | if (synthesizerHandle != 0L) { 97 | return 98 | } 99 | 100 | // create the synthesizer 101 | synthesizerHandle = create() 102 | } 103 | } -------------------------------------------------------------------------------- /app/src/main/java/com/thewolfsound/wavetablesynthesizer/WavetableSynthesizer.kt: -------------------------------------------------------------------------------- 1 | package com.thewolfsound.wavetablesynthesizer 2 | 3 | import androidx.annotation.StringRes 4 | 5 | enum class Wavetable { 6 | SINE { 7 | @StringRes 8 | override fun toResourceString(): Int { 9 | return R.string.sine 10 | } 11 | }, 12 | 13 | TRIANGLE { 14 | @StringRes 15 | override fun toResourceString(): Int { 16 | return R.string.triangle 17 | } 18 | }, 19 | 20 | SQUARE { 21 | @StringRes 22 | override fun toResourceString(): Int { 23 | return R.string.square 24 | } 25 | }, 26 | 27 | SAW { 28 | @StringRes 29 | override fun toResourceString(): Int { 30 | return R.string.sawtooth 31 | } 32 | }; 33 | 34 | @StringRes 35 | abstract fun toResourceString(): Int 36 | } 37 | 38 | interface WavetableSynthesizer { 39 | suspend fun play() 40 | suspend fun stop() 41 | suspend fun isPlaying() : Boolean 42 | suspend fun setFrequency(frequencyInHz: Float) 43 | suspend fun setVolume(volumeInDb: Float) 44 | suspend fun setWavetable(wavetable: Wavetable) 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/java/com/thewolfsound/wavetablesynthesizer/WavetableSynthesizerViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.thewolfsound.wavetablesynthesizer 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.ViewModel 6 | import androidx.lifecycle.viewModelScope 7 | import kotlinx.coroutines.launch 8 | import kotlin.math.exp 9 | import kotlin.math.ln 10 | 11 | 12 | class WavetableSynthesizerViewModel : ViewModel() { 13 | 14 | var wavetableSynthesizer: WavetableSynthesizer? = null 15 | set(value) { 16 | field = value 17 | applyParameters() 18 | } 19 | 20 | private val _frequency = MutableLiveData(300f) 21 | val frequency: LiveData 22 | get() { 23 | return _frequency 24 | } 25 | private val frequencyRange = 40f..3000f 26 | 27 | private val _volume = MutableLiveData(-24f) 28 | val volume: LiveData 29 | get() { 30 | return _volume 31 | } 32 | val volumeRange = (-60f)..0f 33 | 34 | private var wavetable = Wavetable.SINE 35 | 36 | /** 37 | * @param frequencySliderPosition slider position in [0, 1] range 38 | */ 39 | fun setFrequencySliderPosition(frequencySliderPosition: Float) { 40 | val frequencyInHz = frequencyInHzFromSliderPosition(frequencySliderPosition) 41 | _frequency.value = frequencyInHz 42 | viewModelScope.launch { 43 | wavetableSynthesizer?.setFrequency(frequencyInHz) 44 | } 45 | } 46 | 47 | fun setVolume(volumeInDb: Float) { 48 | _volume.value = volumeInDb 49 | viewModelScope.launch { 50 | wavetableSynthesizer?.setVolume(volumeInDb) 51 | } 52 | } 53 | 54 | fun setWavetable(newWavetable: Wavetable) { 55 | wavetable = newWavetable 56 | viewModelScope.launch { 57 | wavetableSynthesizer?.setWavetable(newWavetable) 58 | } 59 | } 60 | 61 | fun playClicked() { 62 | // play() and stop() are suspended functions => we must launch a coroutine 63 | viewModelScope.launch { 64 | if (wavetableSynthesizer?.isPlaying() == true) { 65 | wavetableSynthesizer?.stop() 66 | } else { 67 | wavetableSynthesizer?.play() 68 | } 69 | // Only when the synthesizer changed its state, update the button label. 70 | updatePlayButtonLabel() 71 | } 72 | } 73 | 74 | private fun frequencyInHzFromSliderPosition(sliderPosition: Float): Float { 75 | val rangePosition = linearToExponential(sliderPosition) 76 | return valueFromRangePosition(frequencyRange, rangePosition) 77 | } 78 | 79 | fun sliderPositionFromFrequencyInHz(frequencyInHz: Float): Float { 80 | val rangePosition = rangePositionFromValue(frequencyRange, frequencyInHz) 81 | return exponentialToLinear(rangePosition) 82 | } 83 | 84 | companion object LinearToExponentialConverter { 85 | 86 | private const val MINIMUM_VALUE = 0.001f 87 | fun linearToExponential(value: Float): Float { 88 | assert(value in 0f..1f) 89 | 90 | 91 | if (value < MINIMUM_VALUE) { 92 | return 0f 93 | } 94 | 95 | return exp(ln(MINIMUM_VALUE) - ln(MINIMUM_VALUE) * value) 96 | } 97 | 98 | fun valueFromRangePosition(range: ClosedFloatingPointRange, rangePosition: Float): Float { 99 | assert(rangePosition in 0f..1f) 100 | 101 | return range.start + (range.endInclusive - range.start) * rangePosition 102 | } 103 | 104 | 105 | fun rangePositionFromValue(range: ClosedFloatingPointRange, value: Float): Float { 106 | assert(value in range) 107 | 108 | return (value - range.start) / (range.endInclusive - range.start) 109 | } 110 | 111 | 112 | fun exponentialToLinear(rangePosition: Float): Float { 113 | assert(rangePosition in 0f..1f) 114 | 115 | if (rangePosition < MINIMUM_VALUE) { 116 | return rangePosition 117 | } 118 | 119 | return (ln(rangePosition) - ln(MINIMUM_VALUE)) / (-ln(MINIMUM_VALUE)) 120 | } 121 | } 122 | 123 | private val _playButtonLabel = MutableLiveData(R.string.play) 124 | val playButtonLabel: LiveData 125 | get() { 126 | return _playButtonLabel 127 | } 128 | 129 | fun applyParameters() { 130 | viewModelScope.launch { 131 | wavetableSynthesizer?.setFrequency(frequency.value!!) 132 | wavetableSynthesizer?.setVolume(volume.value!!) 133 | wavetableSynthesizer?.setWavetable(wavetable) 134 | updatePlayButtonLabel() 135 | } 136 | } 137 | 138 | private fun updatePlayButtonLabel() { 139 | viewModelScope.launch { 140 | if (wavetableSynthesizer?.isPlaying() == true) { 141 | _playButtonLabel.value = R.string.stop 142 | } else { 143 | _playButtonLabel.value = R.string.play 144 | } 145 | } 146 | } 147 | } 148 | 149 | -------------------------------------------------------------------------------- /app/src/main/java/com/thewolfsound/wavetablesynthesizer/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package com.thewolfsound.wavetablesynthesizer.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val WolfSoundOrange = Color(0xFFEF7600) 6 | val WolfSoundDarkOrange = Color(0xFF854200) 7 | val WolfSoundGray = Color(0xFF7C7C7C) 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/thewolfsound/wavetablesynthesizer/ui/theme/Shape.kt: -------------------------------------------------------------------------------- 1 | package com.thewolfsound.wavetablesynthesizer.ui.theme 2 | 3 | import androidx.compose.foundation.shape.RoundedCornerShape 4 | import androidx.compose.material.Shapes 5 | import androidx.compose.ui.unit.dp 6 | 7 | val Shapes = Shapes( 8 | small = RoundedCornerShape(4.dp), 9 | medium = RoundedCornerShape(4.dp), 10 | large = RoundedCornerShape(0.dp) 11 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/thewolfsound/wavetablesynthesizer/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.thewolfsound.wavetablesynthesizer.ui.theme 2 | 3 | import androidx.compose.foundation.isSystemInDarkTheme 4 | import androidx.compose.material.MaterialTheme 5 | import androidx.compose.material.darkColors 6 | import androidx.compose.material.lightColors 7 | import androidx.compose.runtime.Composable 8 | 9 | private val DarkColorPalette = darkColors( 10 | primary = WolfSoundOrange, 11 | primaryVariant = WolfSoundDarkOrange, 12 | secondary = WolfSoundGray 13 | ) 14 | 15 | private val LightColorPalette = lightColors( 16 | primary = WolfSoundOrange, 17 | primaryVariant = WolfSoundDarkOrange, 18 | secondary = WolfSoundGray 19 | ) 20 | 21 | @Composable 22 | fun WavetableSynthesizerTheme(darkTheme: Boolean = isSystemInDarkTheme(), 23 | content: @Composable () -> Unit) { 24 | val colors = if (darkTheme) { 25 | DarkColorPalette 26 | } else { 27 | LightColorPalette 28 | } 29 | 30 | MaterialTheme( 31 | colors = colors, 32 | typography = Typography, 33 | shapes = Shapes, 34 | content = content 35 | ) 36 | } -------------------------------------------------------------------------------- /app/src/main/java/com/thewolfsound/wavetablesynthesizer/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package com.thewolfsound.wavetablesynthesizer.ui.theme 2 | 3 | import androidx.compose.material.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.FontFamily 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.unit.sp 8 | 9 | // Set of Material typography styles to start with 10 | val Typography = Typography( 11 | body1 = TextStyle( 12 | fontFamily = FontFamily.Default, 13 | fontWeight = FontWeight.Normal, 14 | fontSize = 16.sp 15 | ) 16 | /* Other default text styles to override 17 | button = TextStyle( 18 | fontFamily = FontFamily.Default, 19 | fontWeight = FontWeight.W500, 20 | fontSize = 14.sp 21 | ), 22 | caption = TextStyle( 23 | fontFamily = FontFamily.Default, 24 | fontWeight = FontWeight.Normal, 25 | fontSize = 12.sp 26 | ) 27 | */ 28 | ) -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 16 | 19 | 22 | 23 | 24 | 25 | 31 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 11 | 16 | 21 | 26 | 31 | 36 | 41 | 46 | 51 | 56 | 61 | 66 | 71 | 76 | 81 | 86 | 91 | 96 | 101 | 106 | 111 | 116 | 121 | 126 | 131 | 136 | 141 | 146 | 151 | 156 | 161 | 166 | 171 | 172 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JanWilczek/android-wavetable-synthesizer/f45ad135fb2def969b4f1726aae8751d548394e1/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JanWilczek/android-wavetable-synthesizer/f45ad135fb2def969b4f1726aae8751d548394e1/app/src/main/res/mipmap-hdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JanWilczek/android-wavetable-synthesizer/f45ad135fb2def969b4f1726aae8751d548394e1/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JanWilczek/android-wavetable-synthesizer/f45ad135fb2def969b4f1726aae8751d548394e1/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JanWilczek/android-wavetable-synthesizer/f45ad135fb2def969b4f1726aae8751d548394e1/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JanWilczek/android-wavetable-synthesizer/f45ad135fb2def969b4f1726aae8751d548394e1/app/src/main/res/mipmap-mdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JanWilczek/android-wavetable-synthesizer/f45ad135fb2def969b4f1726aae8751d548394e1/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JanWilczek/android-wavetable-synthesizer/f45ad135fb2def969b4f1726aae8751d548394e1/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JanWilczek/android-wavetable-synthesizer/f45ad135fb2def969b4f1726aae8751d548394e1/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JanWilczek/android-wavetable-synthesizer/f45ad135fb2def969b4f1726aae8751d548394e1/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JanWilczek/android-wavetable-synthesizer/f45ad135fb2def969b4f1726aae8751d548394e1/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JanWilczek/android-wavetable-synthesizer/f45ad135fb2def969b4f1726aae8751d548394e1/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JanWilczek/android-wavetable-synthesizer/f45ad135fb2def969b4f1726aae8751d548394e1/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JanWilczek/android-wavetable-synthesizer/f45ad135fb2def969b4f1726aae8751d548394e1/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JanWilczek/android-wavetable-synthesizer/f45ad135fb2def969b4f1726aae8751d548394e1/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JanWilczek/android-wavetable-synthesizer/f45ad135fb2def969b4f1726aae8751d548394e1/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JanWilczek/android-wavetable-synthesizer/f45ad135fb2def969b4f1726aae8751d548394e1/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JanWilczek/android-wavetable-synthesizer/f45ad135fb2def969b4f1726aae8751d548394e1/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JanWilczek/android-wavetable-synthesizer/f45ad135fb2def969b4f1726aae8751d548394e1/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JanWilczek/android-wavetable-synthesizer/f45ad135fb2def969b4f1726aae8751d548394e1/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Wavetable Synthesizer 3 | Play 4 | Stop 5 | Frequency 6 | %.1f Hz 7 | Wavetable 8 | Sine 9 | Triangle 10 | Square 11 | Sawtooth 12 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /app/src/test/java/com/thewolfsound/wavetablesynthesizer/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.thewolfsound.wavetablesynthesizer 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | 14 | @Test 15 | fun addition_isCorrect() { 16 | assertEquals(4, 2 + 2) 17 | } 18 | } -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext { 3 | compose_version = '1.1.1' 4 | } 5 | }// Top-level build file where you can add configuration options common to all sub-projects/modules. 6 | plugins { 7 | id 'com.android.application' version '7.1.3' apply false 8 | id 'com.android.library' version '7.1.3' apply false 9 | id 'org.jetbrains.kotlin.android' version '1.6.10' apply false 10 | } 11 | 12 | task clean(type: Delete) { 13 | delete rootProject.buildDir 14 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JanWilczek/android-wavetable-synthesizer/f45ad135fb2def969b4f1726aae8751d548394e1/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Jul 06 21:27:57 CEST 2022 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE.md-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | google() 5 | mavenCentral() 6 | } 7 | } 8 | dependencyResolutionManagement { 9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 10 | repositories { 11 | google() 12 | mavenCentral() 13 | } 14 | } 15 | rootProject.name = "WavetableSynthesizer" 16 | include ':app' 17 | --------------------------------------------------------------------------------