Compare commits
146 Commits
Author | SHA1 | Date | |
---|---|---|---|
65eeade32d | |||
83e101cf8e | |||
|
c779481486 | ||
|
a605293801 | ||
67756cf502 | |||
02e628e5b0 | |||
ff459dece1 | |||
defabafb45 | |||
578bec397f | |||
5c91a48e42 | |||
abc65d292a | |||
d2b15d9155 | |||
90f03881fd | |||
|
5015dd6bbd | ||
724d7c6b8a | |||
da22954810 | |||
f3a12e8e0a | |||
ff99bda751 | |||
b214bc896e | |||
|
6431749bf8 | ||
3a8e279fde | |||
|
2ba329f5ea | ||
|
f1e471e9bf | ||
340d527ffa | |||
dd6f6adb2c | |||
c7f930673f | |||
98e5ef65c4 | |||
daf0cb3e1c | |||
324aa911fd | |||
|
f530db5c45 | ||
|
eaaeebb8ff | ||
adbd1ab61c | |||
750c807db3 | |||
f6b4e1b7ec | |||
b517c83cb5 | |||
9cb3bb00fa | |||
099fb7507b | |||
|
091b677663 | ||
862e56840f | |||
|
6f856ec04c | ||
|
07387edcc6 | ||
|
eef7f35dc5 | ||
|
7b53b045a9 | ||
|
99b6ec2996 | ||
|
e5cc92c72f | ||
f04fcac4ea | |||
c2c1979cc3 | |||
1e34773da8 | |||
f623002c27 | |||
|
2860ddc141 | ||
|
06fc92a2e0 | ||
|
af6ce9ba5e | ||
93ddf112bb | |||
|
5d3fbdbb0a | ||
|
861cdc427e | ||
|
5bd685ed7b | ||
10a2d1ff45 | |||
|
1d731eaafb | ||
|
4dfbded165 | ||
|
5f2a413b4d | ||
|
a4a8ba96ca | ||
|
93f571af18 | ||
|
5a4b9dc9b3 | ||
|
ba9fdc710c | ||
|
e9d80f55ac | ||
|
e0bfcdbdd7 | ||
|
0b64fcbb61 | ||
|
5fcbe57ad0 | ||
|
7a8ef7ae25 | ||
|
4b10a04b3c | ||
|
590dbb8177 | ||
|
442d5ac283 | ||
|
cedcbce960 | ||
|
c39e1df4a5 | ||
|
ed80795731 | ||
|
d3ec90a16e | ||
|
e6cb568b04 | ||
|
3e26125eb4 | ||
|
43d20f5c52 | ||
|
61d3459b95 | ||
|
4f7eb0a82f | ||
|
cbfae97e42 | ||
|
a5e56e2dd1 | ||
|
a80aa87b27 | ||
|
2bb3d1c9f5 | ||
|
da25dcde75 | ||
|
1226e9797d | ||
|
3982f14211 | ||
|
3dcfeaf9fa | ||
|
c9c425e30e | ||
|
ef299091ff | ||
|
5e5a2f0032 | ||
|
b9f49672d1 | ||
|
eff4dab968 | ||
93d960e3b8 | |||
a4924ff832 | |||
d17387f377 | |||
|
763a024f07 | ||
|
4769d090c4 | ||
|
da1a3ef91b | ||
|
14650e8495 | ||
|
65e0e13b55 | ||
|
a4a979ad1d | ||
|
56f98d6d89 | ||
|
2dca881f03 | ||
|
e828804d35 | ||
397c36a7a0 | |||
76343595d8 | |||
9af1146e48 | |||
93df684ac8 | |||
9685ac9b4b | |||
|
74737a8787 | ||
|
17ec290ae6 | ||
|
129d44db76 | ||
|
404530cbbe | ||
|
a9a690bc69 | ||
|
f9e1f5eb73 | ||
|
df5f3837ea | ||
|
0b7da2dda9 | ||
|
1c74f053db | ||
|
745ba9c2dd | ||
|
9b0496474b | ||
|
684f021476 | ||
4ff4ceed70 | |||
|
e4e9643509 | ||
|
1d2f9dda39 | ||
|
2a62abe93b | ||
1b4035b5ac | |||
156374aca4 | |||
0605204593 | |||
4b2f07852b | |||
233c9d7303 | |||
37055e65ad | |||
|
ef8b5626a3 | ||
2ba9b443c1 | |||
|
4ee0c0d0cc | ||
|
2b104fc7f2 | ||
|
e6903eef7f | ||
|
0cb5a29a8b | ||
891fa64c9b | |||
92e312766b | |||
|
df05bf1740 | ||
3ae4947855 | |||
cb37f4f977 | |||
d1e4e0f7c3 | |||
928b0752ff |
28
ChangeLog
28
ChangeLog
@ -1,3 +1,31 @@
|
||||
1.1.0
|
||||
-----
|
||||
* Added new bw_cab module.
|
||||
* Added new fx_cab and fxpp_cab examples.
|
||||
* Added skip_sustain and always_reach_sustain parameters to bw_env_gen.
|
||||
* Added silence_dc parameter to bw_bd_reduce.
|
||||
* Added BW_NULL definition in bw_common and used it throughout the entire
|
||||
codebase.
|
||||
* Added BW_CXX_NO_ARRAY to control the inclusion of features depending on
|
||||
C++ <array>.
|
||||
* Relaxed sidechain APIs in bw_comp and bw_noise_gate to accept BW_NULL to
|
||||
represent null sidechain inputs.
|
||||
* Added setThreshLin() and setThreshDb() methods to the C++ APIs of bw_comp
|
||||
and bw_noise_gate to fix naming typos without breaking the API.
|
||||
* Reworked all example code and added LV2, command line application, and
|
||||
C++/WebAssembly targets.
|
||||
* Fixed gain compensation in bw_satur_process_multi().
|
||||
* Fixed rounding bug in bw_phase_gen when frequency is tiny and negative.
|
||||
* Fixed smoothing of decay parameter in bw_reverb.
|
||||
* Fixed computation of initial states in bw_mm1 and bw_mm2.
|
||||
* Fixed sign-related issues in bw_hash_sdbm(), bw_truncf(), bw_roundf(), and
|
||||
bw_sqrtf() (thanks Kevin Molcard).
|
||||
* Replace GCC pragmas to suppress uninitalized variable warnings with
|
||||
useless harmless statements in bw_env_gen, bw_hs2, bw_ls2, bw_one_pole, and
|
||||
bw_peak.
|
||||
* Fixed documentation typos in bw_ls2.
|
||||
* Updated examples' documentation.
|
||||
|
||||
1.0.0
|
||||
-----
|
||||
* Removed C++ headers and moved C++ code to now-unique C/C++ headers.
|
||||
|
10
README.md
10
README.md
@ -6,18 +6,18 @@ You can find information and documentation [on the official web page](https://ww
|
||||
|
||||
## Subfolders
|
||||
|
||||
* examples: synth and an effect examples in VST3, Web Audio, Daisy Seed, Android app, and iOS app formats;
|
||||
* include: header files.
|
||||
* `examples`: synth and an effect examples in VST3, LV2, Web Audio, Daisy Seed, Android app, iOS app, and command line program formats;
|
||||
* `include`: header files.
|
||||
|
||||
## Legal
|
||||
|
||||
Copyright (C) 2021-2023 Orastron Srl unipersonale.
|
||||
Copyright (C) 2021-2024 Orastron Srl unipersonale.
|
||||
|
||||
Authors: Stefano D'Angelo, Paolo Marrone.
|
||||
|
||||
All the code in the repo is released under GPLv3. See the LICENSE file. Alternatively, we offer a commercial license that doesn't restrict usage with respect to time, projects, or developers involved. More details [on the official web page](https://www.orastron.com/brickworks#license-pricing).
|
||||
All the code in the repo is released under GPLv3. See the `LICENSE` file. Alternatively, we offer a commercial license that doesn't restrict usage with respect to time, projects, or developers involved. More details [on the official web page](https://www.orastron.com/brickworks#license-pricing).
|
||||
|
||||
The file include/bw\_rand.h contains code from https://nullprogram.com/blog/2017/09/21/, which was released into the public domain by its author.
|
||||
The `include/bw_rand.h` file contains code from https://nullprogram.com/blog/2017/09/21/, which was released into the public domain by its author.
|
||||
|
||||
VST is a registered trademark of Steinberg Media Technologies GmbH.
|
||||
|
||||
|
@ -1,5 +1,8 @@
|
||||
We wish to thank and give credit to:
|
||||
- the adopters of this software, of which at the moment we can only publicly mention our friends at [Elk Audio](https://www.elk.audio/);
|
||||
- the adopters of this software, of which at the moment we can publicly mention, in alphabetical order:
|
||||
- [Elk Audio](https://www.elk.audio/);
|
||||
- [Faselunare](http://faselunare.com/);
|
||||
- Kevin Molcard for finding compilation warnings that needed fixing in Brickworks 1.0.0;
|
||||
- users participating to [this thread on the KVR Audio forum](https://www.kvraudio.com/forum/viewtopic.php?f=33&t=589519) for providing useful feedback;
|
||||
- [Mads Kjeldgaard](https://madskjeldgaard.dk/) for publishing [instructions to build for the Daisy Seed and uploading the firmware](https://madskjeldgaard.dk/posts/daisy-setup/);
|
||||
- [Hereket](https://hereket.github.io/) for providing instructions on [how to build an Android app without Android Studio or Gradle](https://hereket.github.io/posts/android_from_command_line/);
|
||||
|
@ -1,34 +1,61 @@
|
||||
# Examples
|
||||
|
||||
Each subfolder contains an example application, except the `common` folder, which contains common code for all examples.
|
||||
## Premise
|
||||
|
||||
In order to build an example just `cd` to `*example*/*platofrm*` and use the following platform-specific instructions.
|
||||
Each of these examples consists of a common part of code, shared by all examples, which contains all necessary boilerplate code and is not Brickworks-related, and a specific part which actually implements the audio engine. The common code is copied/generated by an external tool called [Tibia](https://git.orastron.com/orastron/tibia), which you first need to [run as outlined below](#tibia).
|
||||
|
||||
Building for any platform requires a recent enough version of [GNU Make](https://www.gnu.org/software/make/) installed.
|
||||
Each subfolder contains an example, except the `common` folder, which contains a good deal of common code and common Tibia-related metadata. In order to build an example just `cd` to <code>*example*/*platform*</code> and use the following platform-specific instructions. Building for any platform requires a recent enough version of [GNU Make](https://www.gnu.org/software/make/) installed.
|
||||
|
||||
## Tibia
|
||||
|
||||
### Prerequisites
|
||||
|
||||
You need [Node.js](https://nodejs.org/en) and [npm](https://www.npmjs.com/) to be installed.
|
||||
|
||||
### Usage
|
||||
|
||||
Get [Tibia 0.0.4](https://git.orastron.com/orastron/tibia/releases/tag/v0.0.4), place it in the same directory as the Brickworks folder, and rename it as `tibia`. Then either `cd` to the Tibia folder and `npm install dot`, or install the [dot npm package](https://www.npmjs.com/package/dot) globally and make sure that the `NODE_PATH` environment variable is corretly set to find it.
|
||||
|
||||
Now you can `cd` to the `examples` folder and run `./tibia_gen.sh` to copy/generate files for all examples, or otherwise run `./tibia_gen.sh common` to only copy/generate files in `examples/common` or <code>./tibia\_gen.sh *example*</code> to do the same for files in <code>examples/*example*</code>.
|
||||
|
||||
If you want to remove all files copied/generated by Tibia, and thus restore the `examples` directory as it would appear in the official repository, run `./tibia_clean.sh` from the `examples` directory.
|
||||
## VST3
|
||||
|
||||
### Prerequisites
|
||||
|
||||
Windows (via [MSYS2/Mingw-w64](https://www.msys2.org/)), macOS, and Linux OSes are supported. Building tested with [GCC](https://gcc.gnu.org/), probably also works with [Clang](https://clang.llvm.org/).
|
||||
|
||||
You also need to download the [VST3 SDK](https://www.steinberg.net/developers/) and place it in the same folder as the Brickworks folder, or otherwise edit `common/vst3/vst3.mk` and change the `VST3_SDK_DIR` variable to point to the correct directory.
|
||||
You also need to download or clone the [VST3 C API](https://github.com/steinbergmedia/vst3_c_api) and place it in the same folder as the Brickworks folder, or otherwise edit `common/src/vst3-make.json` and change `cflags` to point to the correct directory then finally [run Tibia](#tibia), or invoke `make` with appropriate `CFLAGS` straight from the command line.
|
||||
|
||||
### Build
|
||||
|
||||
In order to build just type `make`. You'll find the resulting VST3 directory in `build/example.vst3`.
|
||||
In order to build just type `make`. You'll find the resulting VST3 directory in <code>build/bw\_example\_*example*.vst3</code>.
|
||||
|
||||
### Installation
|
||||
|
||||
If all went fine, you can install for the current user (i.e., into the user VST3 folder) by invoking `make install-user`.
|
||||
If all went fine, you can install for the current user (i.e., into the user VST3 folder) by invoking `make install-user` or for all users (i.e., into the system VST3 folder) by `make install`.
|
||||
|
||||
On macOS and Linux you can also install for all users (i.e., into the system VST3 folder) by `make install`.
|
||||
## LV2
|
||||
|
||||
### Prerequisites
|
||||
|
||||
Windows (via [MSYS2/Mingw-w64](https://www.msys2.org/)), macOS, and Linux OSes are supported. Building tested with [GCC](https://gcc.gnu.org/), probably also works with [Clang](https://clang.llvm.org/).
|
||||
|
||||
You also need to download/install [LV2](https://lv2plug.in/), so that either header files are found by the compiler in its default include path, or otherwise you could add an appropriate `cflags` value to `common/src/lv2-make.json` and [run Tibia](#tibia), or invoke `make` with appropriate `CFLAGS` straight from the command line.
|
||||
|
||||
### Build
|
||||
|
||||
In order to build just type `make`. You'll find the resulting LV2 bundle in <code>build/bw\_example\_*example*.lv2</code>.
|
||||
|
||||
### Installation
|
||||
|
||||
If all went fine, you can install for the current user (i.e., into the user VST3 folder) by invoking `make install-user` or for all users (i.e., into the system VST3 folder) by `make install`.
|
||||
|
||||
## Web
|
||||
|
||||
### Prerequisites
|
||||
|
||||
You need Clang with WebAssembly target support and [OpenSSL](https://www.openssl.org/) installed.
|
||||
You need [Clang](https://clang.llvm.org/) with WebAssembly target support and [OpenSSL](https://www.openssl.org/) installed.
|
||||
|
||||
### Build
|
||||
|
||||
@ -44,7 +71,7 @@ The output files need to be served over HTTPS. A self-signed certificate is gene
|
||||
|
||||
Building and firmware upload was only tested on Linux. You need [arm-none-eabi-gcc](https://developer.arm.com/Tools%20and%20Software/GNU%20Toolchain) (for building) and [dfu-util](https://dfu-util.sourceforge.net/) (for firmware upload) installed.
|
||||
|
||||
You also need to clone [libDaisy](https://github.com/electro-smith/libDaisy), `cd` to it, and run `make`. You should either place it in the same folder as the Brickworks folder, or otherwise edit `common/daisy-seed/daisy-seed.mk` and change the `LIBDAISY_DIR` variable to point to the correct directory.
|
||||
You also need to clone/download [libDaisy](https://github.com/electro-smith/libDaisy) (beware that since version 7.0.0 you also need to clone submodules, see the [release notes](https://github.com/electro-smith/libDaisy/releases/tag/v7.0.0)), `cd` to it, and run `make`. You should either place it in the same folder as the Brickworks folder, or otherwise edit `common/src/daisy-seed-make.json` and change `libdaisyDir` to point to the correct directory then finally [run Tibia](#tibia).
|
||||
|
||||
### Build
|
||||
|
||||
@ -64,11 +91,11 @@ Effect examples report output parameter values and CPU usage statistics via USB
|
||||
|
||||
### Prerequisites
|
||||
|
||||
Android examples are built without the help of Android Studio or Gradle. You'll however need to have a recent enough JDK (we need `javac` and `keytool`), as well as to download the latest stable:
|
||||
Android examples are built without the help of Android Studio or Gradle. You'll however need to have a recent enough JDK (we need `javac`), as well as to download the latest stable:
|
||||
|
||||
- Android SDK (https://developer.android.com/studio/index.html) \*;
|
||||
- Android NDK (https://developer.android.com/ndk/downloads) \*;
|
||||
- `.jar`s and `.aar`s (and you'll also need to manually extract the inner `.jar` from each `.aar`, which are just ZIP files) for:
|
||||
- `.jar`s and `.aar`s (and you'll also need to manually extract the inner `classes.jar` from each `xxx.aar`, which are just ZIP files, and rename `classes.jar` to `xxx.jar`) for:
|
||||
- AndroidX Core (https://mvnrepository.com/artifact/androidx.core/core);
|
||||
- AndroidX Lifecycle Common (https://mvnrepository.com/artifact/androidx.lifecycle/lifecycle-common)
|
||||
- AndroidX VersionedParcelable (https://mvnrepository.com/artifact/androidx.versionedparcelable/versionedparcelable)
|
||||
@ -77,7 +104,7 @@ Android examples are built without the help of Android Studio or Gradle. You'll
|
||||
- Koltin Coroutines Core JVM (https://mvnrepository.com/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core-jvm);
|
||||
- `miniaudio.h` library (http://miniaud.io/).
|
||||
|
||||
Then you'll probably also need to adjust paths in `common/android/android.mk`.
|
||||
Then you'll probably also need to adjust paths and values in `common/src/android-make.json` and [run Tibia](#tibia).
|
||||
|
||||
\* You can install both the needed parts of the Android SDK and the NDK by downloading the so-called "command line tools" (https://developer.android.com/studio#command-line-tools-only) and using the included `sdkmanager` program. In such case you need to install the following packages: "platforms;android-*latest*", "build-tools;*latest*", "platform-tools", and "ndk;*latest*".
|
||||
|
||||
@ -87,15 +114,13 @@ In order to build just type `make`. You'll find the resulting `.apk` file in `bu
|
||||
|
||||
### Installation
|
||||
|
||||
If all went fine, you can branch your device and install using `make install`.
|
||||
|
||||
Otherwise, you can also install manually, but please remember to first uninstall the application from the device (`adb install -r` is not sufficient as the signature might have changed while building).
|
||||
If all went fine, you can branch your device and install using `make install` or otherwise install manually.
|
||||
|
||||
### Usage and known issues
|
||||
|
||||
Effect examples process audio input signals, therefore they will require permission to use the capture device.
|
||||
|
||||
Synth examples use input MIDI. While they are coded to support hotplugging, this doesn't seem to work as expected on the devices we tested. You'll need to press "STOP" and then "START" again after plugging a new device.
|
||||
Synth examples use input MIDI and support hotplugging.
|
||||
|
||||
## iOS
|
||||
|
||||
@ -105,7 +130,7 @@ iOS examples are not directly built by the supplied Makefiles. These rather gene
|
||||
|
||||
For this to work you need to have the latest [Xcode](https://developer.apple.com/xcode/) and [XcodeGen](https://github.com/yonaskolb/XcodeGen) installed, as well as a copy of the latest [`miniaudio.h`](http://miniaud.io/).
|
||||
|
||||
Finally, you might need to adjust header search path for miniaudio in `build/common/ios/project.yml`.
|
||||
Finally, you might need to adjust header search path for miniaudio in `common/src/ios-make.json`, `common/src/ios-make-cxx-fx.json`, and `common/src/io-make-cxx-synth.json`, and [run Tibia](#tibia).
|
||||
|
||||
### Build
|
||||
|
||||
@ -120,3 +145,19 @@ At this point you can build and run as with any other iOS app.
|
||||
Effect examples process audio input signals, therefore they will require permission to use the capture device.
|
||||
|
||||
Synth examples use input MIDI and support hotplugging.
|
||||
|
||||
## Command line program
|
||||
|
||||
### Prerequisites
|
||||
|
||||
Windows (via [MSYS2/Mingw-w64](https://www.msys2.org/)), macOS, and Linux OSes are supported. Building tested with [GCC](https://gcc.gnu.org/), probably also works with [Clang](https://clang.llvm.org/).
|
||||
|
||||
Depending on the specific example, you might need to download or clone [tinywav](https://github.com/mhroth/tinywav) and/or [midi-parser](https://github.com/abique/midi-parser) and place them in the same folder as the Brickworks folder, or otherwise edit `common/src/cmd-make.json`
|
||||
|
||||
### Build
|
||||
|
||||
In order to build just type `make`. You'll find the resulting executable file in `build`.
|
||||
|
||||
### Usage
|
||||
|
||||
Just run the executable without arguments to get usage instructions.
|
||||
|
@ -1,15 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.orastron.@NAME@">
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-sdk android:minSdkVersion="26" /> <!-- for androidx core and AAudio -->
|
||||
<uses-sdk android:targetSdkVersion="34" />
|
||||
<application android:label="@NAME@">
|
||||
<activity android:name=".MainActivity" android:label="@NAME@" android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
@ -1,14 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.orastron.@NAME@">
|
||||
<uses-sdk android:minSdkVersion="29" /> <!-- for AMidi -->
|
||||
<uses-sdk android:targetSdkVersion="34" />
|
||||
<application android:label="@NAME@">
|
||||
<activity android:name=".MainActivity" android:label="@NAME@" android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
@ -1,97 +0,0 @@
|
||||
/*
|
||||
* Brickworks
|
||||
*
|
||||
* Copyright (C) 2023 Orastron Srl unipersonale
|
||||
*
|
||||
* Brickworks is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Brickworks is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Brickworks. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* File author: Stefano D'Angelo, Paolo Marrone
|
||||
*/
|
||||
|
||||
package com.orastron.@NAME@;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.webkit.WebView;
|
||||
import android.webkit.WebSettings;
|
||||
import android.webkit.WebChromeClient;
|
||||
import android.webkit.WebViewClient;
|
||||
import android.webkit.JavascriptInterface;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
|
||||
public class MainActivity extends Activity {
|
||||
static {
|
||||
System.loadLibrary("@NAME@");
|
||||
}
|
||||
|
||||
public native boolean nativeAudioStart();
|
||||
public native void nativeAudioStop();
|
||||
public native float nativeGetParameter(int i);
|
||||
public native void nativeSetParameter(int i, float v);
|
||||
|
||||
private WebView webView;
|
||||
|
||||
public class WebAppInterface {
|
||||
@JavascriptInterface
|
||||
public boolean hasAudioPermission() {
|
||||
return MainActivity.this.checkCallingOrSelfPermission(android.Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED;
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public void requestAudioPermission() {
|
||||
ActivityCompat.requestPermissions(MainActivity.this, new String[] { android.Manifest.permission.RECORD_AUDIO }, 0);
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public boolean audioStart() {
|
||||
return nativeAudioStart();
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public void audioStop() {
|
||||
nativeAudioStop();
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public float getParameter(int i) {
|
||||
return nativeGetParameter(i);
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public void setParameter(int i, float v) {
|
||||
nativeSetParameter(i, v);
|
||||
}
|
||||
}
|
||||
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
webView = new WebView(this);
|
||||
setContentView(webView);
|
||||
WebSettings webSettings = webView.getSettings();
|
||||
webSettings.setJavaScriptEnabled(true);
|
||||
webView.setWebChromeClient(new WebChromeClient());
|
||||
webView.setWebViewClient(new WebViewClient());
|
||||
webSettings.setDomStorageEnabled(true);
|
||||
webView.addJavascriptInterface(new WebAppInterface(), "Android");
|
||||
webView.loadUrl("file:///android_asset/index.html");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
|
||||
if (grantResults.length > 0)
|
||||
webView.loadUrl("javascript:gotAudioPermission()");
|
||||
}
|
||||
}
|
@ -1,180 +0,0 @@
|
||||
/*
|
||||
* Brickworks
|
||||
*
|
||||
* Copyright (C) 2023 Orastron Srl unipersonale
|
||||
*
|
||||
* Brickworks is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Brickworks is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Brickworks. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* File author: Stefano D'Angelo, Paolo Marrone
|
||||
*/
|
||||
|
||||
package com.orastron.@NAME@;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.webkit.WebView;
|
||||
import android.webkit.WebSettings;
|
||||
import android.webkit.WebChromeClient;
|
||||
import android.webkit.WebViewClient;
|
||||
import android.webkit.JavascriptInterface;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import android.media.midi.MidiManager;
|
||||
import android.media.midi.MidiManager.DeviceCallback;
|
||||
import android.media.midi.MidiDeviceInfo;
|
||||
import android.media.midi.MidiDeviceInfo.PortInfo;
|
||||
import android.media.midi.MidiDevice;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import android.util.Log;
|
||||
public class MainActivity extends Activity {
|
||||
static {
|
||||
System.loadLibrary("@NAME@");
|
||||
}
|
||||
|
||||
public native boolean nativeAudioStart();
|
||||
public native void nativeAudioStop();
|
||||
public native float nativeGetParameter(int i);
|
||||
public native void nativeSetParameter(int i, float v);
|
||||
public native void addMidiPort(MidiDevice d, int p);
|
||||
public native void removeMidiPort(MidiDevice d, int p);
|
||||
|
||||
private WebView webView;
|
||||
|
||||
public class WebAppInterface {
|
||||
private MidiManager midiManager;
|
||||
private MidiDeviceCallback midiDeviceCallback;
|
||||
public ArrayList<MidiDevice> midiDevices = new ArrayList<MidiDevice>();
|
||||
|
||||
public void addMidiDevices(MidiDeviceInfo[] devices) {
|
||||
for (int i = 0; i < devices.length; i++) {
|
||||
if (devices[i].getOutputPortCount() == 0)
|
||||
continue;
|
||||
midiManager.openDevice(devices[i],
|
||||
new MidiManager.OnDeviceOpenedListener() {
|
||||
@Override
|
||||
public void onDeviceOpened(MidiDevice device) {
|
||||
PortInfo[] ports = device.getInfo().getPorts();
|
||||
for (int i = 0; i < ports.length; i++)
|
||||
if (ports[i].getType() == PortInfo.TYPE_OUTPUT)
|
||||
addMidiPort(device, ports[i].getPortNumber());
|
||||
WebAppInterface.this.midiDevices.add(device);
|
||||
}
|
||||
}, null);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeMidiDevices(MidiDeviceInfo[] devices) {
|
||||
for (int i = 0; i < midiDevices.size(); i++) {
|
||||
MidiDevice device = midiDevices.get(i);
|
||||
int id = device.getInfo().getId();
|
||||
int j = 0;
|
||||
for (; j < devices.length; j++)
|
||||
if (id == devices[j].getId())
|
||||
break;
|
||||
if (j == devices.length)
|
||||
continue;
|
||||
PortInfo[] ports = device.getInfo().getPorts();
|
||||
for (j = 0; j < ports.length; j++)
|
||||
if (ports[j].getType() == PortInfo.TYPE_OUTPUT)
|
||||
removeMidiPort(device, ports[j].getPortNumber());
|
||||
midiDevices.remove(i);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeAllMidiDevices() {
|
||||
for (int i = 0; i < midiDevices.size(); i++) {
|
||||
MidiDevice device = midiDevices.get(i);
|
||||
PortInfo[] ports = device.getInfo().getPorts();
|
||||
for (int j = 0; j < ports.length; j++)
|
||||
if (ports[j].getType() == PortInfo.TYPE_OUTPUT)
|
||||
removeMidiPort(device, ports[j].getPortNumber());
|
||||
}
|
||||
midiDevices.clear();
|
||||
}
|
||||
|
||||
public class MidiDeviceCallback extends MidiManager.DeviceCallback {
|
||||
@Override
|
||||
public void onDeviceAdded(MidiDeviceInfo device) {
|
||||
WebAppInterface.this.addMidiDevices(new MidiDeviceInfo[]{device});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDeviceRemoved(MidiDeviceInfo device) {
|
||||
WebAppInterface.this.removeMidiDevices(new MidiDeviceInfo[]{device});
|
||||
}
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public boolean hasAudioPermission() {
|
||||
return MainActivity.this.checkCallingOrSelfPermission(android.Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED;
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public void requestAudioPermission() {
|
||||
ActivityCompat.requestPermissions(MainActivity.this, new String[] { android.Manifest.permission.RECORD_AUDIO }, 0);
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public boolean audioStart() {
|
||||
midiManager = (MidiManager)getSystemService(Context.MIDI_SERVICE);
|
||||
|
||||
addMidiDevices(midiManager.getDevices());
|
||||
|
||||
midiDeviceCallback = new MidiDeviceCallback();
|
||||
midiManager.registerDeviceCallback(midiDeviceCallback, null);
|
||||
|
||||
return nativeAudioStart();
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public void audioStop() {
|
||||
nativeAudioStop();
|
||||
|
||||
midiManager.unregisterDeviceCallback(midiDeviceCallback);
|
||||
|
||||
removeAllMidiDevices();
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public float getParameter(int i) {
|
||||
return nativeGetParameter(i);
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public void setParameter(int i, float v) {
|
||||
nativeSetParameter(i, v);
|
||||
}
|
||||
}
|
||||
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
webView = new WebView(this);
|
||||
setContentView(webView);
|
||||
WebSettings webSettings = webView.getSettings();
|
||||
webSettings.setJavaScriptEnabled(true);
|
||||
webView.setWebChromeClient(new WebChromeClient());
|
||||
webView.setWebViewClient(new WebViewClient());
|
||||
webSettings.setDomStorageEnabled(true);
|
||||
webView.addJavascriptInterface(new WebAppInterface(), "Android");
|
||||
webView.loadUrl("file:///android_asset/index.html");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
|
||||
if (grantResults.length > 0)
|
||||
webView.loadUrl("javascript:gotAudioPermission()");
|
||||
}
|
||||
}
|
@ -1,138 +0,0 @@
|
||||
COMMON_DIR := ${ROOT_DIR}/../../common/android
|
||||
ANDROID_SDK_DIR := ${HOME}/Android/Sdk
|
||||
ANDROID_NDK_DIR := ${ANDROID_SDK_DIR}/ndk/25.2.9519653
|
||||
ANDROIDX_DIR := ${HOME}/Android/androidx
|
||||
KOTLIN_DIR := ${HOME}/Android/kotlin
|
||||
MINIAUDIO_DIR := ${ROOT_DIR}/../../../../miniaudio
|
||||
|
||||
BUILD_TOOLS_DIR := ${ANDROID_SDK_DIR}/build-tools/34.0.0
|
||||
|
||||
JAR_FILE := ${ANDROID_SDK_DIR}/platforms/android-34/android.jar
|
||||
ANDROIDX_CORE_FILE := ${ANDROIDX_DIR}/core-1.10.1.jar
|
||||
ANDROIDX_LIFECYCLE_COMMON_FILE := ${ANDROIDX_DIR}/lifecycle-common-2.6.1.jar
|
||||
ANDROIDX_VERSIONEDPARCELABLE_FILE := ${ANDROIDX_DIR}/versionedparcelable-1.1.1.jar
|
||||
KOTLIN_STDLIB_FILE := ${KOTLIN_DIR}/kotlin-stdlib-1.9.0.jar
|
||||
KOTLINX_COROUTINES_CORE_FILE := ${KOTLIN_DIR}/kotlinx-coroutines-core-1.7.3.jar
|
||||
KOTLINX_COROUTINES_CORE_JVM_FILE := ${KOTLIN_DIR}/kotlinx-coroutines-core-jvm-1.7.3.jar
|
||||
|
||||
JAVAC := javac
|
||||
KEYTOOL := keytool
|
||||
ifdef SYNTH
|
||||
CXX := ${ANDROID_NDK_DIR}/toolchains/llvm/prebuilt/linux-x86_64/bin/armv7a-linux-androideabi29-clang++
|
||||
else
|
||||
CXX := ${ANDROID_NDK_DIR}/toolchains/llvm/prebuilt/linux-x86_64/bin/armv7a-linux-androideabi26-clang++
|
||||
endif
|
||||
ADB := ${HOME}/Android/Sdk/platform-tools/adb
|
||||
APKSIGNER := ${BUILD_TOOLS_DIR}/apksigner
|
||||
ZIPALIGN := ${BUILD_TOOLS_DIR}/zipalign
|
||||
AAPT := ${BUILD_TOOLS_DIR}/aapt
|
||||
D8 := ${BUILD_TOOLS_DIR}/d8
|
||||
|
||||
CXXFLAGS := \
|
||||
-fPIC \
|
||||
-DNDEBUG \
|
||||
-DBW_NO_DEBUG \
|
||||
-I${ROOT_DIR}/../src \
|
||||
-I${COMMON_DIR} \
|
||||
-I${ROOT_DIR}/../../../include \
|
||||
-I${MINIAUDIO_DIR} \
|
||||
-O3 \
|
||||
-Wall \
|
||||
-Wextra \
|
||||
-Wpedantic \
|
||||
-std=c++11
|
||||
LDFLAGS := \
|
||||
-shared \
|
||||
-static-libstdc++ \
|
||||
-ljnigraphics \
|
||||
-llog \
|
||||
-landroid
|
||||
|
||||
ifdef SYNTH
|
||||
LDFLAGS += -lamidi
|
||||
endif
|
||||
|
||||
SOURCES_COMMON := \
|
||||
build/gen/jni.cpp
|
||||
|
||||
JARS := \
|
||||
${JAR_FILE} \
|
||||
${ANDROIDX_CORE_FILE} \
|
||||
${ANDROIDX_LIFECYCLE_COMMON_FILE} \
|
||||
${ANDROIDX_VERSIONEDPARCELABLE_FILE} \
|
||||
${KOTLIN_STDLIB_FILE} \
|
||||
${KOTLINX_COROUTINES_CORE_FILE} \
|
||||
${KOTLINX_COROUTINES_CORE_JVM_FILE}
|
||||
|
||||
JNI_NAME := $(shell echo ${NAME} | sed 's:_:_1:g')
|
||||
|
||||
ifdef SYNTH
|
||||
ANDROID_MANIFEST_SOURCE := ${COMMON_DIR}/AndroidManifest-synth.xml
|
||||
MAIN_ACTIVITY_SOURCE := ${COMMON_DIR}/MainActivity-synth.java
|
||||
else
|
||||
ANDROID_MANIFEST_SOURCE := ${COMMON_DIR}/AndroidManifest-fx.xml
|
||||
MAIN_ACTIVITY_SOURCE := ${COMMON_DIR}/MainActivity-fx.java
|
||||
endif
|
||||
|
||||
all: build/${NAME}.apk
|
||||
|
||||
build/${NAME}.apk: build/gen/${NAME}.aligned.apk build/apk/lib/armeabi-v7a/lib${NAME}.so build/gen/keystore.jks
|
||||
${APKSIGNER} sign --ks build/gen/keystore.jks --ks-key-alias androidkey --ks-pass pass:android --key-pass pass:android --out $@ build/gen/${NAME}.aligned.apk
|
||||
|
||||
build/gen/keystore.jks: | build/gen
|
||||
${KEYTOOL} -genkeypair -keystore $@ -alias androidkey -dname "CN=orastron.com, OU=ID, O=ORASTRON, L=Abc, S=Xyz, C=IT" -validity 10000 -keyalg RSA -keysize 2048 -storepass android -keypass android
|
||||
|
||||
build/gen/${NAME}.aligned.apk: build/gen/${NAME}.unsigned.apk
|
||||
${ZIPALIGN} -f -p 4 $^ $@
|
||||
|
||||
build/gen/${NAME}.unsigned.apk: build/apk/classes.dex build/gen/AndroidManifest.xml build/assets/index.html build/assets/config.js build/apk/lib/armeabi-v7a/lib${NAME}.so| build/gen
|
||||
${AAPT} package -f -M build/gen/AndroidManifest.xml -A build/assets -I ${JAR_FILE} -I ${ANDROIDX_CORE_FILE} -I ${ANDROIDX_LIFECYCLE_COMMON_FILE} -I ${ANDROIDX_VERSIONEDPARCELABLE_FILE} -I ${KOTLIN_STDLIB_FILE} -I ${KOTLINX_COROUTINES_CORE_FILE} -I ${KOTLINX_COROUTINES_CORE_JVM_FILE} -F $@ build/apk
|
||||
|
||||
ifdef SYNTH
|
||||
build/apk/classes.dex: build/apk/my_classes.jar
|
||||
cd build/apk && ${D8} --min-api 29 ../../$^ ${JARS} && cd ../..
|
||||
|
||||
build/apk/my_classes.jar: build/obj/com/orastron/${NAME}/MainActivity.class build/obj/com/orastron/${NAME}/MainActivity$$WebAppInterface.class build/obj/com/orastron/${NAME}/MainActivity$$WebAppInterface$$MidiDeviceCallback.class build/obj/com/orastron/${NAME}/MainActivity$$WebAppInterface$$1.class | build/apk
|
||||
${D8} build/obj/com/orastron/${NAME}/MainActivity.class 'build/obj/com/orastron/${NAME}/MainActivity$$WebAppInterface.class' 'build/obj/com/orastron/${NAME}/MainActivity$$WebAppInterface$$MidiDeviceCallback.class' 'build/obj/com/orastron/${NAME}/MainActivity$$WebAppInterface$$1.class' --min-api 29 --output $@ --no-desugaring
|
||||
else
|
||||
build/apk/classes.dex: build/apk/my_classes.jar
|
||||
cd build/apk && ${D8} --min-api 26 ../../$^ ${JARS} && cd ../..
|
||||
|
||||
build/apk/my_classes.jar: build/obj/com/orastron/${NAME}/MainActivity.class build/obj/com/orastron/${NAME}/MainActivity$$WebAppInterface.class | build/apk
|
||||
${D8} build/obj/com/orastron/${NAME}/MainActivity.class 'build/obj/com/orastron/${NAME}/MainActivity$$WebAppInterface.class' --min-api 26 --output $@ --no-desugaring
|
||||
endif
|
||||
|
||||
build/apk/lib/armeabi-v7a/lib${NAME}.so: ${SOURCES} | build/apk/lib/armeabi-v7a
|
||||
${CXX} $^ ${CXXFLAGS} ${LDFLAGS} -o $@
|
||||
|
||||
build/gen/jni.cpp: ${COMMON_DIR}/jni.cpp | build/gen
|
||||
cat $^ | sed s:@JNI_NAME@:${JNI_NAME}:g > $@
|
||||
|
||||
build/obj/com/orastron/${NAME}/MainActivity$$WebAppInterface.class: build/obj/com/orastron/${NAME}/MainActivity.class
|
||||
|
||||
build/obj/com/orastron/${NAME}/MainActivity.class: build/gen/com/orastron/${NAME}/MainActivity.java | build/obj
|
||||
${JAVAC} -classpath "${JAR_FILE}:${ANDROIDX_CORE_FILE}:${ANDROIDX_LIFECYCLE_COMMON_FILE}:${ANDROIDX_VERSIONEDPARCELABLE_FILE}:${KOTLIN_STDLIB_FILE}:${KOTLINX_COROUTINES_CORE_FILE}:${KOTLINX_COROUTINES_CORE_JVM_FILE}" -d build/obj build/gen/com/orastron/${NAME}/MainActivity.java
|
||||
|
||||
build/gen/com/orastron/${NAME}/MainActivity.java: ${MAIN_ACTIVITY_SOURCE} | build/gen/com/orastron/${NAME}
|
||||
cat $^ | sed s:@NAME@:${NAME}:g > $@
|
||||
|
||||
build/gen/AndroidManifest.xml: ${ANDROID_MANIFEST_SOURCE} | build/gen/com/orastron/${NAME}
|
||||
cat $^ | sed s:@NAME@:${NAME}:g > $@
|
||||
|
||||
build/assets/index.html: ${COMMON_DIR}/index.html | build/assets
|
||||
cp $^ $@
|
||||
|
||||
build/assets/config.js: ${ROOT_DIR}/../src/config.js | build/assets
|
||||
cp $^ $@
|
||||
|
||||
build/apk build/apk/lib/armeabi-v7a build/obj build/gen/com/orastron/${NAME} build/gen build/assets:
|
||||
mkdir -p $@
|
||||
|
||||
clean:
|
||||
rm -fr build
|
||||
|
||||
install: build/${NAME}.apk
|
||||
[ -n "`${ADB} shell pm list packages | grep ^package:com.orastron.${NAME}$$`" ] && ${ADB} uninstall com.orastron.${NAME}; exit 0
|
||||
${ADB} install $^
|
||||
|
||||
.PHONY: all clean install
|
@ -1,139 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
|
||||
Brickworks
|
||||
|
||||
Copyright (C) 2023 Orastron Srl unipersonale
|
||||
|
||||
Brickworks is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, version 3 of the License.
|
||||
|
||||
Brickworks is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Brickworks. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
File author: Stefano D'Angelo, Paolo Marrone
|
||||
|
||||
-->
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
|
||||
<script type="text/javascript" src="config.js"></script>
|
||||
<script type="text/javascript">
|
||||
var hasAudioPermission = true;
|
||||
for (var i = 0; i < buses.length; i++)
|
||||
if (!buses[i].output) {
|
||||
hasAudioPermission = Android.hasAudioPermission();
|
||||
break;
|
||||
}
|
||||
var audioStarted = false;
|
||||
var topButtonElem;
|
||||
var outParamInterval;
|
||||
|
||||
window.onload = function () {
|
||||
topButtonElem = document.getElementById("topButton");
|
||||
var paramsElem = document.getElementById("params");
|
||||
|
||||
topButtonElem.value = hasAudioPermission ? "START" : "INIT";
|
||||
topButtonElem.addEventListener("click", function () {
|
||||
if (hasAudioPermission) {
|
||||
if (audioStarted) {
|
||||
clearInterval(outParamInterval);
|
||||
Android.audioStop();
|
||||
|
||||
paramsElem.innerHTML = "";
|
||||
|
||||
topButtonElem.value = "START";
|
||||
audioStarted = false;
|
||||
} else {
|
||||
if (Android.audioStart()) {
|
||||
for (var i = 0; i < parameters.length; i++) {
|
||||
var div = document.createElement("div");
|
||||
paramsElem.appendChild(div);
|
||||
|
||||
var label = document.createElement("label");
|
||||
label.innerText = parameters[i].name;
|
||||
div.appendChild(label);
|
||||
|
||||
div.appendChild(document.createElement("br"));
|
||||
|
||||
var range = document.createElement("input");
|
||||
range.classList.add("range");
|
||||
range.setAttribute("type", "range");
|
||||
range.setAttribute("id", "p" + i);
|
||||
range.setAttribute("min", 0);
|
||||
range.setAttribute("max", 1);
|
||||
range.setAttribute("step", parameters[i].step ? 1 / parameters[i].step : "any");
|
||||
range.value = parameters[i].defaultValue;
|
||||
if (parameters[i].output)
|
||||
range.setAttribute("readonly", "readonly");
|
||||
else {
|
||||
let index = i;
|
||||
range.addEventListener("input",
|
||||
function (ev) {
|
||||
Android.setParameter(index, parseFloat(ev.target.value));
|
||||
});
|
||||
}
|
||||
div.appendChild(range);
|
||||
}
|
||||
|
||||
outParamInterval = setInterval(
|
||||
function () {
|
||||
for (var i = 0; i < parameters.length; i++)
|
||||
if (parameters[i].output)
|
||||
document.getElementById("p" + i).value = Android.getParameter(i);
|
||||
}, 50);
|
||||
|
||||
topButtonElem.value = "STOP";
|
||||
audioStarted = true;
|
||||
} else
|
||||
alert("Could not start audio");
|
||||
}
|
||||
}
|
||||
else
|
||||
Android.requestAudioPermission();
|
||||
});
|
||||
};
|
||||
|
||||
function gotAudioPermission() {
|
||||
hasAudioPermission = true;
|
||||
topButtonElem.value = "START";
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 1em;
|
||||
}
|
||||
|
||||
#topButton {
|
||||
width: 100%;
|
||||
border: 0;
|
||||
background-color: #04aa6d;
|
||||
color: white;
|
||||
padding: 0.5em;
|
||||
text-align: center;
|
||||
margin-bottom: 1em;
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
.range {
|
||||
width: 90%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||