├── .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 |
4 |
5 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 | GETTERS_AND_SETTERS
108 | KEEP
109 |
110 |
111 | OVERRIDDEN_METHODS
112 | KEEP
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 | true
121 | true
122 | true
123 | true
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 | true
133 | true
134 | true
135 | true
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 | true
145 | true
146 | true
147 | true
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 | true
157 | true
158 | true
159 | true
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 | true
169 | true
170 | true
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 | true
180 | true
181 | true
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 | true
191 | true
192 | true
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 | true
202 | true
203 | true
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 | true
213 | true
214 | true
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 | true
224 | true
225 | true
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 | true
235 | true
236 | true
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 | true
246 | true
247 | true
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 | true
257 | true
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 | true
267 | true
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 | true
277 | true
278 |
279 |
280 |
281 |
282 |
283 |
284 |
285 |
286 | true
287 | true
288 |
289 |
290 |
291 |
292 |
293 |
294 |
295 | true
296 |
297 |
298 |
299 |
300 |
301 |
302 | true
303 |
304 |
305 |
306 |
307 |
308 |
309 | true
310 |
311 |
312 |
313 |
314 |
315 |
316 |
317 | true
318 | true
319 |
320 |
321 |
322 |
323 |
324 |
325 |
326 | true
327 |
328 |
329 |
330 |
331 |
332 |
333 | true
334 |
335 |
336 |
337 |
338 |
339 |
340 |
341 |
342 |
343 |
344 |
345 |
346 |
347 |
348 | true
349 | true
350 | true
351 |
352 |
353 |
354 |
355 |
356 |
357 |
358 |
359 | true
360 | true
361 | true
362 |
363 |
364 |
365 |
366 |
367 |
368 |
369 |
370 | true
371 | true
372 | true
373 |
374 |
375 |
376 |
377 |
378 |
379 |
380 |
381 | true
382 | true
383 | true
384 |
385 |
386 |
387 |
388 |
389 |
390 |
391 |
392 | true
393 | true
394 |
395 |
396 |
397 |
398 |
399 |
400 |
401 |
402 | true
403 | true
404 |
405 |
406 |
407 |
408 |
409 |
410 |
411 |
412 | true
413 | true
414 |
415 |
416 |
417 |
418 |
419 |
420 |
421 |
422 | true
423 | true
424 |
425 |
426 |
427 |
428 |
429 |
430 |
431 | true
432 |
433 |
434 |
435 |
436 |
437 |
438 |
439 | true
440 | true
441 |
442 |
443 |
444 |
445 |
446 |
447 |
448 | true
449 |
450 |
451 |
452 |
453 |
454 |
455 |
456 |
457 |
458 |
459 |
460 |
461 |
462 |
463 |
464 |
465 |
466 |
467 |
468 |
469 |
470 |
471 |
472 |
473 |
474 |
475 |
476 |
477 |
478 |
479 |
480 |
481 |
482 |
483 |
484 | xmlns:android
485 | ^$
486 |
487 |
488 |
489 |
490 |
491 |
492 |
493 |
494 | xmlns:.*
495 | ^$
496 |
497 |
498 | BY_NAME
499 |
500 |
501 |
502 |
503 |
504 |
505 | .*:id
506 | http://schemas.android.com/apk/res/android
507 |
508 |
509 |
510 |
511 |
512 |
513 |
514 |
515 | .*:name
516 | http://schemas.android.com/apk/res/android
517 |
518 |
519 |
520 |
521 |
522 |
523 |
524 |
525 | name
526 | ^$
527 |
528 |
529 |
530 |
531 |
532 |
533 |
534 |
535 | style
536 | ^$
537 |
538 |
539 |
540 |
541 |
542 |
543 |
544 |
545 | .*
546 | ^$
547 |
548 |
549 | BY_NAME
550 |
551 |
552 |
553 |
554 |
555 |
556 | .*
557 | http://schemas.android.com/apk/res/android
558 |
559 |
560 | ANDROID_ATTRIBUTE_ORDER
561 |
562 |
563 |
564 |
565 |
566 |
567 | .*
568 | .*
569 |
570 |
571 | BY_NAME
572 |
573 |
574 |
575 |
576 |
577 |
578 |
579 |
580 |
581 |
582 |
583 |
584 |
585 |
586 |
587 |
588 |
589 |
590 |
591 |
592 |
593 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
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 | 
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 |
--------------------------------------------------------------------------------