├── .circleci
├── config-github-pages.yml
└── config.yml
├── .github
└── dependabot.yml
├── .gitignore
├── LICENSE
├── README.md
├── android
├── .gitignore
├── build.gradle.kts
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── kotlin
│ │ └── it
│ │ └── krzeminski
│ │ └── fsynth
│ │ └── ExampleInstrumentedTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ ├── kotlin
│ │ └── it
│ │ │ └── krzeminski
│ │ │ └── fsynth
│ │ │ └── MainActivity.kt
│ └── res
│ │ ├── drawable-v24
│ │ └── ic_launcher_foreground.xml
│ │ ├── drawable
│ │ └── ic_launcher_background.xml
│ │ ├── layout
│ │ └── activity_main.xml
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ └── values
│ │ ├── colors.xml
│ │ ├── strings.xml
│ │ └── styles.xml
│ └── test
│ └── kotlin
│ └── it
│ └── krzeminski
│ └── fsynth
│ └── ExampleUnitTest.kt
├── build.gradle.kts
├── cli
├── build.gradle.kts
└── src
│ ├── main
│ └── kotlin
│ │ └── it
│ │ └── krzeminski
│ │ └── fsynth
│ │ ├── ConsoleEntryPoint.kt
│ │ ├── JvmSoundPlayback.kt
│ │ └── SoundOutputRendering.kt
│ └── test
│ └── kotlin
│ └── performance
│ └── RenderingTimeAndNumberOfSegments.kt
├── core
├── build.gradle.kts
└── src
│ ├── commonMain
│ └── kotlin
│ │ └── it
│ │ └── krzeminski
│ │ ├── fsynth
│ │ ├── PrimitiveWaveGenerators.kt
│ │ ├── Rendering.kt
│ │ ├── development
│ │ │ └── Metronome.kt
│ │ ├── effects
│ │ │ └── envelope
│ │ │ │ └── AdsrEnvelope.kt
│ │ ├── instruments
│ │ │ ├── Instrument.kt
│ │ │ ├── Organs.kt
│ │ │ ├── Percussion.kt
│ │ │ └── Synthesizer.kt
│ │ ├── postprocessing
│ │ │ └── QualityReduction.kt
│ │ ├── songs
│ │ │ ├── PinkPantherThemeIntro.kt
│ │ │ ├── SimpleDemoSong.kt
│ │ │ ├── SongRepository.kt
│ │ │ └── VanHalenJumpIntro.kt
│ │ ├── synthesis
│ │ │ ├── Preprocessing.kt
│ │ │ ├── Synthesis.kt
│ │ │ ├── caching
│ │ │ │ └── bucketing
│ │ │ │ │ ├── BucketedTrack.kt
│ │ │ │ │ └── BucketedTrackBuilding.kt
│ │ │ └── types
│ │ │ │ └── SongForSynthesis.kt
│ │ ├── types
│ │ │ ├── BoundedWaveform.kt
│ │ │ ├── MusicNote.kt
│ │ │ ├── PositionedBoundedWaveform.kt
│ │ │ ├── Song.kt
│ │ │ ├── SongBuildingDsl.kt
│ │ │ ├── SynthesisParameters.kt
│ │ │ └── Waveform.kt
│ │ └── worker
│ │ │ ├── README.md
│ │ │ └── SynthesisWorker.kt
│ │ └── gitinfo
│ │ └── types
│ │ └── GitInfo.kt
│ └── commonTest
│ └── kotlin
│ └── it
│ └── krzeminski
│ └── fsynth
│ ├── PrimitiveWaveGeneratorsTest.kt
│ ├── RenderingTest.kt
│ ├── effects
│ └── envelope
│ │ └── AdsrEnvelopeTest.kt
│ ├── postprocessing
│ └── QualityReductionTest.kt
│ ├── synthesis
│ ├── PreprocessingTest.kt
│ ├── SynthesisTest.kt
│ └── caching
│ │ └── bucketing
│ │ └── BucketedTrackBuildingTest.kt
│ ├── testutils
│ └── BoundedWaveformComparison.kt
│ └── types
│ ├── BoundedWaveformTest.kt
│ ├── MusicNoteTest.kt
│ ├── SongBuildingDslTest.kt
│ └── WaveformTest.kt
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── ktlint.gradle.kts
├── settings.gradle.kts
└── web
├── build.gradle.kts
├── serviceworker
├── build.gradle.kts
└── src
│ └── main
│ └── kotlin
│ └── it
│ └── krzeminski
│ └── fsynth
│ └── web
│ └── serviceworker
│ └── main.kt
├── src
└── main
│ ├── kotlin
│ └── it
│ │ └── krzeminski
│ │ ├── fsynth
│ │ ├── PlaybackCustomization.kt
│ │ ├── Player.kt
│ │ ├── SynthesisWorkerProxy.kt
│ │ ├── VersionInfo.kt
│ │ ├── Wavesurfer.kt
│ │ ├── main.kt
│ │ └── typings
│ │ │ ├── AudioBufferToWav.kt
│ │ │ ├── AudioRelatedClasses.kt
│ │ │ └── WaveSurfer.kt
│ │ └── testutils
│ │ └── TimeMeasurement.kt
│ └── resources
│ ├── Logo.svg
│ ├── index.html
│ └── manifest.webmanifest
├── webpack.config.d
└── hot-reload-fix.js
└── worker
├── build.gradle.kts
└── src
└── main
└── kotlin
└── it
└── krzeminski
└── fsynth
└── web
└── worker
├── WebSoundRendering.kt
├── main.kt
├── testutils
└── TimeMeasurement.kt
└── typings
└── AudioRelatedClasses.kt
/.circleci/config-github-pages.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | jobs:
3 | build:
4 | docker:
5 | - image: circleci/openjdk:8-jdk
6 | steps:
7 | - run: echo "No-op"
8 |
--------------------------------------------------------------------------------
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | jobs:
3 | core:
4 | docker:
5 | - image: circleci/openjdk:8-jdk-node-browsers
6 | working_directory: ~/repo
7 | steps:
8 | - checkout
9 | - run: ./gradlew --version
10 | - run: ./gradlew :core:build
11 | android:
12 | docker:
13 | - image: circleci/android:api-29
14 | working_directory: ~/repo
15 | steps:
16 | - checkout
17 | - run: ./gradlew --version
18 | - run: ./gradlew :android:build
19 | cli:
20 | docker:
21 | - image: circleci/openjdk:8-jdk
22 | working_directory: ~/repo
23 | steps:
24 | - checkout
25 | - run: ./gradlew --version
26 | - run: ./gradlew :cli:build
27 | - run:
28 | name: Publish code coverage info
29 | command: bash <(curl -s https://codecov.io/bash)
30 | web:
31 | docker:
32 | - image: circleci/openjdk:8-jdk-node-browsers
33 | working_directory: ~/repo
34 | environment:
35 | CHROME_BIN: "/usr/bin/google-chrome"
36 | steps:
37 | - checkout
38 | - run: ./gradlew --version
39 | - run: ./gradlew :web:build
40 | - run:
41 | name: Publish to GitHub Pages
42 | command: |
43 | set -e
44 |
45 | if [ $CIRCLE_BRANCH != "master" ]
46 | then
47 | echo "Not on master branch - skipping"
48 | exit 0
49 | fi
50 |
51 | git config --global user.email "builds@circleci.com"
52 | git config --global user.name "CircleCI"
53 |
54 | git clone --single-branch --branch gh-pages https://${GITHUB_TOKEN}@github.com/${CIRCLE_PROJECT_USERNAME}/${CIRCLE_PROJECT_REPONAME}.git gh-pages
55 | cd gh-pages
56 | git checkout --orphan gh-pages-new
57 | git rm -rf .
58 | mkdir .circleci
59 |
60 | cd ..
61 | cp -av web/build/distributions gh-pages
62 | cp .circleci/config-github-pages.yml gh-pages/.circleci/config.yml
63 | cd gh-pages
64 | mv distributions/* .
65 |
66 | git add -A
67 | git commit -m "Circle CI deployment to GitHub Pages: ${CIRCLE_SHA1}" --allow-empty
68 | git branch -D gh-pages
69 | git branch -m gh-pages-new gh-pages
70 | git push --force origin gh-pages
71 |
72 | workflows:
73 | version: 2
74 | Tests:
75 | jobs:
76 | - core
77 | - android
78 | - cli
79 | - web
80 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: "gradle"
9 | directory: "/"
10 | schedule:
11 | interval: "daily"
12 | - package-ecosystem: "gradle"
13 | directory: "/core"
14 | schedule:
15 | interval: "daily"
16 | - package-ecosystem: "gradle"
17 | directory: "/web"
18 | schedule:
19 | interval: "daily"
20 | - package-ecosystem: "gradle"
21 | directory: "/android"
22 | schedule:
23 | interval: "daily"
24 | - package-ecosystem: "gradle"
25 | directory: "/cli"
26 | schedule:
27 | interval: "daily"
28 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .idea/shelf
3 | /android.tests.dependencies
4 | /confluence/target
5 | /dependencies
6 | /dist
7 | /local
8 | /gh-pages
9 | /ideaSDK
10 | /clionSDK
11 | /android-studio/sdk
12 | out/
13 | /tmp
14 | workspace.xml
15 | *.versionsBackup
16 | /idea/testData/debugger/tinyApp/classes*
17 | /jps-plugin/testData/kannotator
18 | /ultimate/dependencies
19 | /ultimate/ideaSDK
20 | /ultimate/out
21 | /ultimate/tmp
22 | /js/js.translator/testData/out/
23 | /js/js.translator/testData/out-min/
24 | .gradle/
25 | build/
26 | !**/src/**/build
27 | !**/test/**/build
28 | *.iml
29 | !**/testData/**/*.iml
30 | .idea/
31 | target/
32 | local.properties
33 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Piotr Krzemiński
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://app.circleci.com/pipelines/github/krzema12/fsynth) [](https://codecov.io/gh/krzema12/fsynth) [](https://github.com/KotlinBy/awesome-kotlin)
2 |
3 |
4 |
5 | # What is fsynth?
6 |
7 | It's a simple music synthesizer. It focuses on generating the waveforms from scratch, no samples are used. You can listen to it via the Web player, or using the cross-platform Java CLI. Read on to learn more.
8 |
9 | Secondly, this project is my playground for learning Kotlin, functional programming (hence the "f" in "fsynth"), front-end development, Gradle, multiplatform projects, and other. It's also a place where I can focus more on quality than I normally could afford in a professional environment, because here I don't have a pressure to deliver on time. That's why my aim here is also to have as little technical debt as possible, and have as clean code as I can write.
10 |
11 | Songs are described with such DSL:
12 |
13 | ```kotlin
14 | val simpleDemoSong = song(
15 | name = "Simple demo song",
16 | beatsPerMinute = 120) {
17 | track(instrument = organs, volume = 0.3f) {
18 | note(1 by 8, D4)
19 | note(1 by 16, Csharp4)
20 | note(1 by 16, D4)
21 | note(1 by 8, E4)
22 | note(1 by 8, D4)
23 |
24 | pause(1 by 8)
25 | chord(1 by 8, A3, D4, Fsharp4)
26 | chord(1 by 4, B3, D4, G4)
27 | }
28 | }
29 | ```
30 |
31 | # How to listen
32 |
33 | ## Web
34 |
35 | https://fsynthlib.github.io/fsynth/
36 |
37 | ## Java CLI
38 |
39 | The CLI uses system sound output to play music.
40 |
41 | You can use the CLI from the distribution package:
42 |
43 | ```
44 | ./gradlew :cli:installDist
45 | cli/build/install/fsynth/bin/fsynth --song 'Van Halen - Jump (intro)'
46 | ```
47 |
48 | or during development, you can call the CLI through Gradle:
49 |
50 | ```
51 | ./gradlew :cli:run --args="--song 'Van Halen - Jump (intro)'"
52 | ```
53 |
54 | To see a list of available songs, call the CLI without arguments.
55 |
56 | # Talk on [Tricity Kotlin User Group](https://www.facebook.com/groups/tricity.kotlin.user.group)
57 |
58 | [![[TKUG #1] Piotr Krzemiński, 'Project fsynth: show and tell'](http://img.youtube.com/vi/mQEIn7Eqsio/0.jpg)](http://www.youtube.com/watch?v=mQEIn7Eqsio "[TKUG #1] Piotr Krzemiński, 'Project fsynth: show and tell'")
59 |
60 | # Build prerequisites
61 |
62 | The below dependencies won't be installed by Gradle:
63 |
64 | * JDK + path to it in `JAVA_HOME`
65 | (warning: use Oracle's JDK for now, there are some issue with OpenJDK; see #31 for details)
66 | * Android SDK + path to it in `ANDROID_HOME`
67 |
--------------------------------------------------------------------------------
/android/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/android/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.android.application")
3 | id("kotlin-android")
4 | id("kotlin-android-extensions")
5 | }
6 |
7 | android {
8 | compileSdkVersion(29)
9 | buildToolsVersion("29.0.2")
10 | defaultConfig {
11 | applicationId = "it.krzeminski.fsynth"
12 | minSdkVersion(26)
13 | targetSdkVersion(29)
14 | versionCode = 1
15 | versionName = "1.0"
16 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
17 | }
18 | buildTypes {
19 | getByName("release") {
20 | isMinifyEnabled = false
21 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
22 | }
23 | }
24 | kotlinOptions {
25 | jvmTarget = JavaVersion.VERSION_1_8.toString()
26 | }
27 | }
28 |
29 | repositories {
30 | google()
31 | jcenter()
32 | }
33 |
34 | val kotlinVersion: String by rootProject.extra
35 |
36 | dependencies {
37 | implementation(fileTree(Pair("dir", "libs"), Pair("include", listOf("*.jar"))))
38 | implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion")
39 | implementation("androidx.appcompat:appcompat:1.2.0")
40 | implementation("androidx.core:core-ktx:1.3.2")
41 | implementation("androidx.constraintlayout:constraintlayout:2.0.4")
42 | implementation("com.google.android.material:material:1.3.0")
43 | implementation(project(":core"))
44 | testImplementation("junit:junit:4.13.2")
45 | androidTestImplementation("androidx.test:runner:1.3.0")
46 | androidTestImplementation("androidx.test.espresso:espresso-core:3.3.0")
47 | }
48 |
--------------------------------------------------------------------------------
/android/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.kts.
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
22 |
--------------------------------------------------------------------------------
/android/src/androidTest/kotlin/it/krzeminski/fsynth/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package it.krzeminski.fsynth
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.assertEquals
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 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("it.krzeminski.fsynth", appContext.packageName)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/android/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/android/src/main/kotlin/it/krzeminski/fsynth/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package it.krzeminski.fsynth
2 |
3 | import android.media.AudioFormat
4 | import android.media.AudioManager
5 | import android.media.AudioTrack
6 | import android.media.AudioTrack.WRITE_BLOCKING
7 | import androidx.appcompat.app.AppCompatActivity
8 | import android.os.Bundle
9 | import androidx.core.view.plusAssign
10 | import com.google.android.material.button.MaterialButton
11 | import it.krzeminski.fsynth.songs.allSongs
12 | import it.krzeminski.fsynth.types.Song
13 | import kotlinx.android.synthetic.main.activity_main.*
14 |
15 | class MainActivity : AppCompatActivity() {
16 |
17 | override fun onCreate(savedInstanceState: Bundle?) {
18 | super.onCreate(savedInstanceState)
19 | setContentView(R.layout.activity_main)
20 |
21 | allSongs.forEach { song ->
22 | buttons += MaterialButton(this, null, R.attr.materialButtonOutlinedStyle).apply {
23 | text = song.name
24 | setOnClickListener {
25 | song.playOnAndroid(sampleRate = 44100)
26 | }
27 | }
28 | }
29 | }
30 | }
31 |
32 | private fun Song.playOnAndroid(sampleRate: Int) {
33 | val samples = renderWithSampleRate(sampleRate).toList()
34 | val audioTrack = AudioTrack(
35 | AudioManager.STREAM_MUSIC,
36 | sampleRate,
37 | AudioFormat.CHANNEL_OUT_MONO,
38 | AudioFormat.ENCODING_PCM_FLOAT,
39 | samples.size*java.lang.Float.BYTES,
40 | AudioTrack.MODE_STATIC)
41 | with(audioTrack) {
42 | write(samples.toFloatArray(), 0, samples.size, WRITE_BLOCKING)
43 | play()
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/android/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
12 |
13 |
19 |
22 |
25 |
26 |
27 |
28 |
34 |
35 |
--------------------------------------------------------------------------------
/android/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
10 |
12 |
14 |
16 |
18 |
20 |
22 |
24 |
26 |
28 |
30 |
32 |
34 |
36 |
38 |
40 |
42 |
44 |
46 |
48 |
50 |
52 |
54 |
56 |
58 |
60 |
62 |
64 |
66 |
68 |
70 |
72 |
74 |
75 |
--------------------------------------------------------------------------------
/android/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
14 |
15 |
--------------------------------------------------------------------------------
/android/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/android/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/android/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fsynthlib/fsynth/1f138bf5c5d46a80b23eca8bde589da1b865662a/android/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fsynthlib/fsynth/1f138bf5c5d46a80b23eca8bde589da1b865662a/android/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/android/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fsynthlib/fsynth/1f138bf5c5d46a80b23eca8bde589da1b865662a/android/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fsynthlib/fsynth/1f138bf5c5d46a80b23eca8bde589da1b865662a/android/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/android/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fsynthlib/fsynth/1f138bf5c5d46a80b23eca8bde589da1b865662a/android/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fsynthlib/fsynth/1f138bf5c5d46a80b23eca8bde589da1b865662a/android/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/android/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fsynthlib/fsynth/1f138bf5c5d46a80b23eca8bde589da1b865662a/android/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fsynthlib/fsynth/1f138bf5c5d46a80b23eca8bde589da1b865662a/android/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/android/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fsynthlib/fsynth/1f138bf5c5d46a80b23eca8bde589da1b865662a/android/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fsynthlib/fsynth/1f138bf5c5d46a80b23eca8bde589da1b865662a/android/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/android/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #008577
4 | #00574B
5 | #D81B60
6 |
7 |
--------------------------------------------------------------------------------
/android/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | fsynth
3 |
4 |
--------------------------------------------------------------------------------
/android/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/android/src/test/kotlin/it/krzeminski/fsynth/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package it.krzeminski.fsynth
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.assertEquals
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 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | buildscript {
2 | val kotlinVersion by extra { "1.4.30" }
3 |
4 | repositories {
5 | google()
6 | mavenCentral()
7 | maven("https://plugins.gradle.org/m2/")
8 | }
9 |
10 | dependencies {
11 | classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
12 | // As a workaround, the root project knows about the Android project.
13 | // Otherwise, there are some issues with building the Android project.
14 | classpath("com.android.tools.build:gradle:3.5.0")
15 | }
16 | }
17 |
18 | allprojects {
19 | repositories {
20 | mavenCentral()
21 | }
22 |
23 | tasks.withType> {
24 | kotlinOptions {
25 | freeCompilerArgs = freeCompilerArgs + "-progressive"
26 | }
27 | }
28 | }
29 |
30 | subprojects {
31 | apply(from = "$rootDir/ktlint.gradle.kts")
32 | }
33 |
--------------------------------------------------------------------------------
/cli/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | kotlin("jvm")
3 | application
4 | jacoco
5 | }
6 |
7 |
8 | val kotlinVersion: String by rootProject.extra
9 |
10 | dependencies {
11 | implementation("org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion")
12 | implementation("com.github.ajalt:clikt:1.7.0")
13 | implementation(project(":core"))
14 |
15 | testImplementation("junit:junit:4.13.2")
16 | testImplementation("org.jetbrains.kotlin:kotlin-test")
17 | testImplementation("org.jetbrains.kotlin:kotlin-test-junit")
18 | }
19 |
20 | application {
21 | mainClassName = "it.krzeminski.fsynth.ConsoleEntryPointKt"
22 | applicationName = "fsynth"
23 | }
24 |
25 | val compileKotlin: org.jetbrains.kotlin.gradle.tasks.KotlinCompile by tasks
26 | compileKotlin.kotlinOptions.jvmTarget = "1.8"
27 |
28 | jacoco {
29 | toolVersion = "0.8.3"
30 | }
31 |
32 | val jacocoTestReport = tasks.getByName("jacocoTestReport") {
33 | reports {
34 | xml.isEnabled = true
35 | html.isEnabled = true
36 | }
37 |
38 | afterEvaluate {
39 | classDirectories.setFrom(files(classDirectories.files.map {
40 | fileTree(it) {
41 | exclude(
42 | // Excluding songs and instruments from code coverage.
43 | // Rationale: testing songs would be like testing MIDI files - some pieces of data.
44 | // Regarding instruments, they are defined as math operations on simpler waveforms and testing them
45 | // would bring no benefit, however operations alone (addition, multiplication) are still tested.
46 | "it/krzeminski/fsynth/songs/**",
47 | "it/krzeminski/fsynth/instruments/**")
48 | }
49 | }))
50 | }
51 | }
52 |
53 | tasks.getByName("check").dependsOn(jacocoTestReport)
54 |
--------------------------------------------------------------------------------
/cli/src/main/kotlin/it/krzeminski/fsynth/ConsoleEntryPoint.kt:
--------------------------------------------------------------------------------
1 | package it.krzeminski.fsynth
2 |
3 | import com.github.ajalt.clikt.core.CliktCommand
4 | import com.github.ajalt.clikt.parameters.options.default
5 | import com.github.ajalt.clikt.parameters.options.option
6 | import com.github.ajalt.clikt.parameters.options.required
7 | import com.github.ajalt.clikt.parameters.options.validate
8 | import com.github.ajalt.clikt.parameters.types.choice
9 | import com.github.ajalt.clikt.parameters.types.float
10 | import com.github.ajalt.clikt.parameters.types.path
11 | import it.krzeminski.fsynth.generated.gitInfo
12 | import it.krzeminski.fsynth.songs.allSongs
13 | import it.krzeminski.fsynth.types.Song
14 | import java.nio.file.Path
15 | import java.time.Instant
16 |
17 | fun main(args: Array) = Synthesize().main(args)
18 |
19 | private class Synthesize : CliktCommand(name = "fsynth") {
20 | val song: Song by option(
21 | "--song",
22 | help = "Name of the song to play",
23 | metavar = "NAME")
24 | .choice(allSongs.map { it.name to it }.toMap())
25 | .required()
26 |
27 | val startTime: Float by option(
28 | help = "Optional. Starts the payback omitting the given amount of seconds",
29 | metavar = "SECONDS")
30 | .float()
31 | .default(0.0f)
32 | .validate {
33 | require(it >= 0.0f) {
34 | "Start time should be positive!"
35 | }
36 | }
37 |
38 | val outputFile: Path? by option(
39 | help = "Optional. If given, the song will not be played, but instead, written as a WAVE file",
40 | metavar = "PATH")
41 | .path()
42 |
43 | override fun run() {
44 | printIntroduction()
45 |
46 | val audioStream = song.asAudioStream(samplesPerSecond = 44100, sampleSizeInBits = 8, startTime = startTime)
47 |
48 | val outputFileFinal = outputFile
49 | if (outputFileFinal != null) {
50 | audioStream?.saveAsWaveFile(outputFileFinal)
51 | } else {
52 | audioStream?.playAndBlockUntilFinishes()
53 | }
54 | }
55 | }
56 |
57 | private fun printIntroduction() {
58 | println("fsynth by Piotr Krzemiński")
59 | println("Version ${gitInfo.latestCommit.sha1.substring(0, 8)} " +
60 | "from ${Instant.ofEpochSecond(gitInfo.latestCommit.timeUnixTimestamp)}")
61 | }
62 |
--------------------------------------------------------------------------------
/cli/src/main/kotlin/it/krzeminski/fsynth/JvmSoundPlayback.kt:
--------------------------------------------------------------------------------
1 | package it.krzeminski.fsynth
2 |
3 | import it.krzeminski.fsynth.types.Song
4 | import java.io.ByteArrayInputStream
5 | import java.nio.file.Path
6 | import javax.sound.sampled.AudioFileFormat
7 | import javax.sound.sampled.AudioFormat
8 | import javax.sound.sampled.AudioInputStream
9 | import javax.sound.sampled.AudioSystem
10 | import javax.sound.sampled.Clip
11 | import javax.sound.sampled.LineEvent
12 | import kotlin.system.measureTimeMillis
13 |
14 | fun Song.asAudioStream(samplesPerSecond: Int, sampleSizeInBits: Int, startTime: Float): AudioInputStream? {
15 | require(sampleSizeInBits == 8) { "Only 8-bit samples are supported now!" }
16 |
17 | lateinit var rawData: ByteArray
18 | val synthesisTimeInMilliseconds = measureTimeMillis {
19 | rawData = render8bit(song = this, sampleRate = samplesPerSecond, startTime = startTime)
20 | }
21 |
22 | if (rawData.isEmpty()) {
23 | println("No song data to play, returning a null AudioInputStream")
24 | return null
25 | }
26 |
27 | println("Synthesized in ${synthesisTimeInMilliseconds.toFloat() / 1000.0f} s")
28 | val audioFormat = buildAudioFormat(samplesPerSecond, sampleSizeInBits)
29 | return prepareAudioInputStream(rawData, audioFormat)
30 | }
31 |
32 | fun AudioInputStream.playAndBlockUntilFinishes() {
33 | println("Playing...")
34 | val audioInputStream = this
35 | with(AudioSystem.getClip()) {
36 | open(audioInputStream)
37 | start()
38 | sleepUntilPlaybackFinishes()
39 | }
40 | }
41 |
42 | fun AudioInputStream.saveAsWaveFile(path: Path) {
43 | AudioSystem.write(this, AudioFileFormat.Type.WAVE, path.toFile())
44 | }
45 |
46 | private object AudioFormatConstants {
47 | const val MONO = 1
48 | const val UNSIGNED = false
49 | const val LITTLE_ENDIAN = false
50 | }
51 |
52 | private fun buildAudioFormat(samplesPerSecond: Int, sampleSizeInBits: Int): AudioFormat {
53 | return AudioFormat(
54 | samplesPerSecond.toFloat(),
55 | sampleSizeInBits,
56 | AudioFormatConstants.MONO,
57 | AudioFormatConstants.UNSIGNED,
58 | AudioFormatConstants.LITTLE_ENDIAN)
59 | }
60 |
61 | private fun prepareAudioInputStream(rawData: ByteArray, audioFormat: AudioFormat): AudioInputStream {
62 | return AudioInputStream(
63 | ByteArrayInputStream(rawData),
64 | audioFormat,
65 | rawData.size.toLong())
66 | }
67 |
68 | private fun Clip.sleepUntilPlaybackFinishes() {
69 | var continuePlayback = true
70 | addLineListener { event ->
71 | if (event.type == LineEvent.Type.STOP) {
72 | continuePlayback = false
73 | }
74 | }
75 |
76 | while (continuePlayback) {
77 | Thread.sleep(100)
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/cli/src/main/kotlin/it/krzeminski/fsynth/SoundOutputRendering.kt:
--------------------------------------------------------------------------------
1 | package it.krzeminski.fsynth
2 |
3 | import it.krzeminski.fsynth.types.Song
4 |
5 | fun render8bit(song: Song, sampleRate: Int, startTime: Float): ByteArray =
6 | song.renderWithSampleRate(sampleRate, startTime)
7 | .map { waveformValue -> normalizedWaveValueToByte(waveformValue) }
8 | .toList()
9 | .toByteArray()
10 |
11 | /**
12 | * Takes [value] of the sound wave, between -1.0 and 1.0, and returns an 8-bit sample (e.g. 0 for -1.0, 255 for 1.0).
13 | */
14 | private fun normalizedWaveValueToByte(value: Float): Byte =
15 | (((value + 1.0f) / 2.0f) * 255.0f).toByte()
16 |
--------------------------------------------------------------------------------
/cli/src/test/kotlin/performance/RenderingTimeAndNumberOfSegments.kt:
--------------------------------------------------------------------------------
1 | package performance
2 |
3 | import it.krzeminski.fsynth.instruments.synthesizer
4 | import it.krzeminski.fsynth.renderWithSampleRate
5 | import it.krzeminski.fsynth.types.MusicNote
6 | import it.krzeminski.fsynth.types.by
7 | import it.krzeminski.fsynth.types.song
8 | import kotlin.system.measureTimeMillis
9 |
10 | fun main(args: Array) {
11 | (1..1000).forEach { numberOfSegments ->
12 | val testSong = song(name = "Test song", beatsPerMinute = 100) {
13 | track(instrument = synthesizer, volume = 1.0f) {
14 | repeat(numberOfSegments) {
15 | note(1 by 4, MusicNote.A4)
16 | }
17 | }
18 | }
19 | val timeMillis = measureTimeMillis {
20 | testSong.renderWithSampleRate(44100).forEach { }
21 | }
22 | println("Segments: $numberOfSegments, time in ms: $timeMillis")
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/core/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import org.ajoberstar.grgit.Grgit
2 |
3 | plugins {
4 | kotlin("multiplatform")
5 | id("org.ajoberstar.grgit") version "4.1.0"
6 | }
7 |
8 | kotlin {
9 | jvm {
10 | }
11 | js {
12 | browser()
13 | }
14 | sourceSets {
15 | val commonMain by getting {
16 | dependencies {
17 | implementation("org.jetbrains.kotlin:kotlin-stdlib-common")
18 | }
19 | kotlin.srcDirs(kotlin.srcDirs, "$buildDir/generated/")
20 | }
21 | val commonTest by getting {
22 | dependencies {
23 | implementation("org.jetbrains.kotlin:kotlin-test-common")
24 | implementation("org.jetbrains.kotlin:kotlin-test-annotations-common")
25 | implementation("it.krzeminski.vis-assert:vis-assert:0.4.1-beta")
26 | }
27 | }
28 | val jvmMain by getting {
29 | dependencies {
30 | implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
31 | }
32 | }
33 | val jvmTest by getting {
34 | dependencies {
35 | implementation("org.jetbrains.kotlin:kotlin-test")
36 | implementation("org.jetbrains.kotlin:kotlin-test-junit")
37 | }
38 | }
39 | val jsMain by getting {
40 | dependencies {
41 | implementation("org.jetbrains.kotlin:kotlin-stdlib-js")
42 | }
43 | }
44 | val jsTest by getting {
45 | dependencies {
46 | implementation("org.jetbrains.kotlin:kotlin-test-js")
47 | }
48 | }
49 | }
50 | }
51 |
52 | val generateGitInfo = tasks.register("generateGitInfo") {
53 | val grgit = Grgit.open(mapOf("currentDir" to project.rootDir))
54 | val fileDirectory = "$buildDir/generated/it/krzeminski/fsynth/generated"
55 | val fileName = "GitInfo.kt"
56 |
57 | val sha1 = grgit.head().id
58 | val timeUnixTimestamp = grgit.head().dateTime.toEpochSecond()
59 | val containsUncommittedChanges = !grgit.status().isClean
60 |
61 | val contents = """
62 | package it.krzeminski.fsynth.generated
63 |
64 | import it.krzeminski.gitinfo.types.CommitMetadata
65 | import it.krzeminski.gitinfo.types.GitInfo
66 |
67 | val gitInfo = GitInfo(
68 | latestCommit = CommitMetadata(
69 | sha1 = "$sha1",
70 | timeUnixTimestamp = $timeUnixTimestamp),
71 | containsUncommittedChanges = $containsUncommittedChanges)
72 | """.trimIndent()
73 | doLast {
74 | file(fileDirectory).mkdirs()
75 | file("$fileDirectory/$fileName").writeText(contents)
76 | }
77 | }
78 |
79 | // There's no easy way to express that 'generateGitInfo' task needs to be executed before any code is compiled.
80 | // That's why a dependency on this task is added to all known tasks that deal with compiling code, so that this file
81 | // already exists when code for any platform is about to be compiled.
82 | tasks.getByName("compileKotlinJs").dependsOn(generateGitInfo)
83 | tasks.getByName("compileKotlinJvm").dependsOn(generateGitInfo)
84 | tasks.getByName("compileKotlinMetadata").dependsOn(generateGitInfo)
85 |
--------------------------------------------------------------------------------
/core/src/commonMain/kotlin/it/krzeminski/fsynth/PrimitiveWaveGenerators.kt:
--------------------------------------------------------------------------------
1 | package it.krzeminski.fsynth
2 |
3 | import it.krzeminski.fsynth.types.Waveform
4 | import kotlin.math.PI
5 | import kotlin.math.sin
6 |
7 | val silence = { _: Float -> 0.0f }
8 |
9 | fun sineWave(frequency: Float): Waveform = { t -> sin(frequency*t*2.0*PI).toFloat() }
10 |
11 | fun squareWave(frequency: Float): Waveform = { t -> if (sineWave(frequency)(t) >= 0.0f) 1.0f else -1.0f }
12 |
--------------------------------------------------------------------------------
/core/src/commonMain/kotlin/it/krzeminski/fsynth/Rendering.kt:
--------------------------------------------------------------------------------
1 | package it.krzeminski.fsynth
2 |
3 | import it.krzeminski.fsynth.synthesis.buildSongEvaluationFunction
4 | import it.krzeminski.fsynth.synthesis.durationInSeconds
5 | import it.krzeminski.fsynth.synthesis.preprocessForSynthesis
6 | import it.krzeminski.fsynth.synthesis.types.SongForSynthesis
7 | import it.krzeminski.fsynth.types.Song
8 |
9 | fun Song.renderWithSampleRate(sampleRate: Int, startTime: Float = 0.0f, onProgressChange: (Int) -> Unit = {}) =
10 | this.preprocessForSynthesis()
11 | .renderWithSampleRate(sampleRate, startTime, onProgressChange)
12 |
13 | fun SongForSynthesis.renderWithSampleRate(
14 | sampleRate: Int,
15 | startTime: Float,
16 | onProgressChange: (Int) -> Unit = {}
17 | ): Sequence {
18 | val startSample = (sampleRate.toFloat() * startTime).toInt()
19 | val endSample = (sampleRate.toFloat() * durationInSeconds).toInt()
20 | val samplesToRender = endSample - startSample
21 | val songEvaluationFunction = buildSongEvaluationFunction()
22 | return (startSample..endSample).asSequence().chunked((samplesToRender / 10).coerceAtLeast(1)).map { chunk ->
23 | onProgressChange((chunk[0] - startSample)*100 / samplesToRender)
24 | chunk
25 | .map(sampleIndexToTimeInSeconds(sampleRate))
26 | .map(songEvaluationFunction)
27 | }.flatten()
28 | }
29 |
30 | private fun sampleIndexToTimeInSeconds(sampleRate: Int) = { sampleIndex: Int -> sampleIndex.toFloat() / sampleRate.toFloat() }
31 |
--------------------------------------------------------------------------------
/core/src/commonMain/kotlin/it/krzeminski/fsynth/development/Metronome.kt:
--------------------------------------------------------------------------------
1 | package it.krzeminski.fsynth.development
2 |
3 | import it.krzeminski.fsynth.instruments.synthesizer
4 | import it.krzeminski.fsynth.types.MusicNote.C4
5 | import it.krzeminski.fsynth.types.MusicNote.C5
6 | import it.krzeminski.fsynth.types.NoteValue
7 | import it.krzeminski.fsynth.types.Song
8 | import it.krzeminski.fsynth.types.Track
9 | import it.krzeminski.fsynth.types.TrackSegment
10 |
11 | fun Song.add4by4MetronomeTrackForDevelopment(trackSegments: Int) =
12 | this.copy(tracks = tracks + Track(
13 | name = "Metronome",
14 | instrument = synthesizer,
15 | volume = 0.15f,
16 | segments = generateTrackSegments(trackSegments)))
17 |
18 | private fun generateTrackSegments(trackSegments: Int) =
19 | (0..(trackSegments - 1)).asSequence()
20 | .flatMap {
21 | listOf(C5, C4, C4, C4)
22 | .flatMap { note ->
23 | listOf(
24 | TrackSegment.SingleNote(NoteValue(1, 32), note),
25 | TrackSegment.Pause(NoteValue(7, 32)))
26 | }
27 | .asSequence()
28 | }
29 | .toList()
30 |
--------------------------------------------------------------------------------
/core/src/commonMain/kotlin/it/krzeminski/fsynth/effects/envelope/AdsrEnvelope.kt:
--------------------------------------------------------------------------------
1 | package it.krzeminski.fsynth.effects.envelope
2 |
3 | import it.krzeminski.fsynth.types.BoundedWaveform
4 |
5 | /**
6 | * Describes parameters of an Attack-Decay-Sustain-Release envelope.
7 | * All time parameters are given in seconds.
8 | * @see Wikipedia, Envelope (music)#ADSR.
9 | */
10 | data class AdsrEnvelopeDefinition(
11 | val attackTime: Float,
12 | val decayTime: Float,
13 | val sustainLevel: Float,
14 | val releaseTime: Float
15 | )
16 |
17 | fun buildEnvelopeFunction(definition: AdsrEnvelopeDefinition) = { keyPressDuration: Float -> adsrEnvelope(keyPressDuration, definition) }
18 |
19 | fun adsrEnvelope(keyPressDuration: Float, definition: AdsrEnvelopeDefinition): BoundedWaveform {
20 | with(definition) {
21 | require(keyPressDuration >= 0.0f) { "Key press duration must not be negative!" }
22 | require(attackTime >= 0.0f) { "Attack time must not be negative!" }
23 | require(decayTime >= 0.0f) { "Decay time must not be negative!" }
24 | require(sustainLevel >= 0.0f) { "Sustain level must not be negative!" }
25 | require(releaseTime >= 0.0f) { "Release time must not be negative!" }
26 |
27 | fun envelopeForPressedKey(t: Float): Float {
28 | return when (t) {
29 | in 0.0f..attackTime -> when {
30 | attackTime != 0.0f -> t / attackTime
31 | decayTime != 0.0f -> 1.0f
32 | else -> sustainLevel
33 | }
34 | in attackTime..(attackTime + decayTime) -> 1.0f - (1.0f - sustainLevel)*(t - attackTime) / decayTime
35 | else -> sustainLevel
36 | }
37 | }
38 | val envelopeAtKeyRelease = envelopeForPressedKey(keyPressDuration)
39 | val actualReleaseTime = if (sustainLevel != 0.0f) releaseTime * envelopeAtKeyRelease / sustainLevel else 0.0f
40 | val waveform = { t: Float ->
41 | when (t) {
42 | in 0.0f..keyPressDuration -> envelopeForPressedKey(t)
43 | else -> envelopeAtKeyRelease - sustainLevel*(t - keyPressDuration) / releaseTime
44 | }
45 | }
46 | return BoundedWaveform(waveform, keyPressDuration + actualReleaseTime)
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/core/src/commonMain/kotlin/it/krzeminski/fsynth/instruments/Instrument.kt:
--------------------------------------------------------------------------------
1 | package it.krzeminski.fsynth.instruments
2 |
3 | import it.krzeminski.fsynth.types.BoundedWaveform
4 | import it.krzeminski.fsynth.types.Waveform
5 |
6 | data class Instrument(
7 | val waveform: (Frequency) -> Waveform,
8 | val envelope: (KeyPressDuration) -> BoundedWaveform
9 | )
10 |
11 | typealias Frequency = Float
12 | typealias KeyPressDuration = Float
13 |
--------------------------------------------------------------------------------
/core/src/commonMain/kotlin/it/krzeminski/fsynth/instruments/Organs.kt:
--------------------------------------------------------------------------------
1 | package it.krzeminski.fsynth.instruments
2 |
3 | import it.krzeminski.fsynth.effects.envelope.AdsrEnvelopeDefinition
4 | import it.krzeminski.fsynth.effects.envelope.buildEnvelopeFunction
5 | import it.krzeminski.fsynth.sineWave
6 | import it.krzeminski.fsynth.types.plus
7 | import it.krzeminski.fsynth.types.times
8 |
9 | val organs = Instrument(
10 | waveform = { frequency: Frequency ->
11 | 0.3 * sineWave(2 * frequency) +
12 | 0.7 * sineWave(frequency) +
13 | 0.2 * sineWave(0.5f * frequency)
14 | },
15 | envelope = buildEnvelopeFunction(
16 | AdsrEnvelopeDefinition(
17 | attackTime = 0.05f,
18 | decayTime = 0.0f,
19 | sustainLevel = 1.0f,
20 | releaseTime = 0.3f))
21 | )
22 |
--------------------------------------------------------------------------------
/core/src/commonMain/kotlin/it/krzeminski/fsynth/instruments/Percussion.kt:
--------------------------------------------------------------------------------
1 | package it.krzeminski.fsynth.instruments
2 |
3 | import it.krzeminski.fsynth.effects.envelope.AdsrEnvelopeDefinition
4 | import it.krzeminski.fsynth.effects.envelope.buildEnvelopeFunction
5 | import kotlin.random.Random
6 |
7 | val cymbals = Instrument(
8 | waveform = { _: Frequency ->
9 | with(Random(0)) {
10 | { _ -> this.nextFloat() }
11 | }
12 | },
13 | envelope = buildEnvelopeFunction(
14 | AdsrEnvelopeDefinition(
15 | attackTime = 0.001f,
16 | decayTime = 0.0f,
17 | sustainLevel = 1.0f,
18 | releaseTime = 0.05f))
19 | )
20 |
--------------------------------------------------------------------------------
/core/src/commonMain/kotlin/it/krzeminski/fsynth/instruments/Synthesizer.kt:
--------------------------------------------------------------------------------
1 | package it.krzeminski.fsynth.instruments
2 |
3 | import it.krzeminski.fsynth.effects.envelope.AdsrEnvelopeDefinition
4 | import it.krzeminski.fsynth.effects.envelope.buildEnvelopeFunction
5 | import it.krzeminski.fsynth.sineWave
6 | import it.krzeminski.fsynth.squareWave
7 | import it.krzeminski.fsynth.types.plus
8 | import it.krzeminski.fsynth.types.times
9 |
10 | val synthesizer = Instrument(
11 | waveform = { frequency: Frequency ->
12 | 0.3 * squareWave(frequency) +
13 | 0.2 * squareWave(frequency*2) +
14 | 0.5 * sineWave(frequency)
15 | },
16 | envelope = buildEnvelopeFunction(
17 | AdsrEnvelopeDefinition(
18 | attackTime = 0.05f,
19 | decayTime = 0.0f,
20 | sustainLevel = 1.0f,
21 | releaseTime = 0.01f))
22 | )
23 |
24 | val simpleDecayEnvelopeSynthesizer = synthesizer.copy(
25 | envelope = buildEnvelopeFunction(
26 | AdsrEnvelopeDefinition(
27 | attackTime = 0.05f,
28 | decayTime = 5.0f,
29 | sustainLevel = 0.2f,
30 | releaseTime = 0.005f))
31 | )
32 |
--------------------------------------------------------------------------------
/core/src/commonMain/kotlin/it/krzeminski/fsynth/postprocessing/QualityReduction.kt:
--------------------------------------------------------------------------------
1 | package it.krzeminski.fsynth.postprocessing
2 |
3 | import kotlin.math.roundToInt
4 |
5 | /**
6 | * Given some sample value as [Float] (that is, 32-bit precision), converts the value so that the whole <-1.0; 1.0>
7 | * range is divided into [targetLevels] buckets. For example, if [targetLevels] is 2, all values below 0 will become -1,
8 | * and above 0 will become 1.
9 | *
10 | * This function is useful to simulate what would happen if there was less bits per sample e.g. in some data
11 | * transmission, when considering sound. It allows checking what is the quality degradation given the amount of data
12 | * designated for the number of levels per sample.
13 | */
14 | fun Float.reduceLevelsPerSample(targetLevels: Int): Float {
15 | val maxValue = targetLevels - 1
16 | val normalizedInput = (this + 1.0f) / 2.0f
17 | val valueScaledToMax = normalizedInput * maxValue
18 | val rounded = valueScaledToMax.roundToInt().toFloat()
19 | return rounded * 2.0f / maxValue - 1.0f
20 | }
21 |
--------------------------------------------------------------------------------
/core/src/commonMain/kotlin/it/krzeminski/fsynth/songs/PinkPantherThemeIntro.kt:
--------------------------------------------------------------------------------
1 | package it.krzeminski.fsynth.songs
2 |
3 | import it.krzeminski.fsynth.instruments.cymbals
4 | import it.krzeminski.fsynth.instruments.simpleDecayEnvelopeSynthesizer
5 | import it.krzeminski.fsynth.instruments.synthesizer
6 | import it.krzeminski.fsynth.types.MusicNote.* // ktlint-disable no-wildcard-imports
7 | import it.krzeminski.fsynth.types.by
8 | import it.krzeminski.fsynth.types.to
9 | import it.krzeminski.fsynth.types.song
10 |
11 | val pinkPantherThemeIntro = song(name = "Pink Panther Theme (intro)", beatsPerMinute = 120) {
12 | track(name = "Main melody", instrument = synthesizer, volume = 0.3f) {
13 | pause(1 by 1)
14 | pause(1 by 1)
15 | pause(1 by 1)
16 | pause(1 by 1)
17 |
18 | pause(11 by 12)
19 | note(1 by 12, Dsharp4)
20 |
21 | note(5 by 12, E4)
22 | note(1 by 12, Fsharp4)
23 | note(5 by 12, G4)
24 | note(1 by 12, Dsharp4)
25 |
26 | note(2 by 12, E4)
27 | note(1 by 12, Fsharp4)
28 | note(2 by 12, G4)
29 | note(1 by 12, C5)
30 | note(2 by 12, B4)
31 | note(1 by 12, E4)
32 | note(2 by 12, G4)
33 | note(1 by 12, B4)
34 |
35 | glissando(7 by 12, A4 to Asharp4)
36 | glissando(1 by 12, G4 to A4)
37 | note(1 by 12, G4)
38 | note(1 by 12, E4)
39 | note(1 by 12, D4)
40 | note(10 by 12, E4)
41 | pause(2 by 12)
42 | note(1 by 12, Dsharp4)
43 |
44 | note(5 by 12, E4)
45 | note(1 by 12, Fsharp4)
46 | note(5 by 12, G4)
47 | note(1 by 12, Dsharp4)
48 |
49 | note(2 by 12, E4)
50 | note(1 by 12, Fsharp4)
51 | note(2 by 12, G4)
52 | note(1 by 12, C5)
53 | note(2 by 12, B4)
54 | note(1 by 12, G4)
55 | note(2 by 12, B4)
56 | note(1 by 12, E5)
57 |
58 | note(1 by 1, Dsharp5)
59 |
60 | pause(1 by 1)
61 |
62 | pause(3 by 12)
63 | glissando(2 by 12, D5 to E5)
64 | note(1 by 12, D5)
65 | glissando(2 by 12, A4 to B4)
66 | note(1 by 12, A4)
67 | note(2 by 12, G4)
68 | note(1 by 12, E4)
69 |
70 | glissando(3 by 12, Asharp4 to A4)
71 | glissando(3 by 12, Asharp4 to A4)
72 | glissando(3 by 12, Asharp4 to A4)
73 | glissando(3 by 12, Asharp4 to A4)
74 |
75 | note(1 by 12, G4)
76 | note(1 by 12, E4)
77 | note(1 by 12, D4)
78 | note(1 by 12, E4)
79 | pause(1 by 12)
80 | note(7 by 12, E4)
81 | }
82 |
83 | track(name = "Chord background", instrument = synthesizer, volume = 0.1f) {
84 | repeat(2) {
85 | pause(8 by 12)
86 | chord(1 by 12, Csharp3, Gsharp3)
87 | chord(2 by 12, D3, A3)
88 | chord(1 by 12, Dsharp3, Asharp3)
89 |
90 | chord(1 by 1, E3, B3)
91 | }
92 |
93 | repeat(2) {
94 | pause(8 by 12)
95 | chord(1 by 12, Csharp3, Gsharp3)
96 | chord(2 by 12, D3, A3)
97 | chord(1 by 12, Dsharp3, Asharp3)
98 |
99 | chord(1 by 1, E3, B3)
100 | pause(8 by 12)
101 | chord(1 by 12, E3, B3)
102 | chord(2 by 12, Dsharp3, Asharp3)
103 | chord(1 by 12, D3, A3)
104 |
105 | chord(1 by 1, Csharp3, Gsharp3)
106 | }
107 |
108 | pause(8 by 12)
109 | chord(1 by 12, Csharp3, Gsharp3)
110 | chord(2 by 12, D3, A3)
111 | chord(1 by 12, Dsharp3, Asharp3)
112 |
113 | chord(1 by 4, E3, B3)
114 | pause(3 by 4)
115 |
116 | pause(1 by 1)
117 |
118 | chord(1 by 1, E3, B3)
119 | }
120 |
121 | track(name = "Bass", instrument = simpleDecayEnvelopeSynthesizer, volume = 0.1f) {
122 | pause(1 by 1)
123 |
124 | repeat(2) {
125 | note(1 by 2, E3)
126 | note(1 by 2, B2)
127 | note(1 by 2, E2)
128 | pause(1 by 2)
129 | }
130 |
131 | repeat(2) {
132 | note(1 by 2, E3)
133 | note(1 by 2, B2)
134 | note(1 by 2, E2)
135 | pause(1 by 2)
136 |
137 | note(1 by 2, Csharp3)
138 | note(1 by 2, Gsharp2)
139 | note(1 by 2, Csharp2)
140 | pause(1 by 2)
141 | }
142 | }
143 |
144 | track(name = "Percussion", instrument = cymbals, volume = 0.1f) {
145 | fun repeatingPattern() {
146 | note(1 by 12, A4)
147 | pause(2 by 12)
148 | note(1 by 12, A4)
149 | pause(1 by 12)
150 | note(1 by 24, A4)
151 | pause(1 by 24)
152 | }
153 |
154 | pause(1 by 1)
155 |
156 | repeat(24) {
157 | repeatingPattern()
158 | }
159 |
160 | note(1 by 12, A4)
161 | pause(11 by 12)
162 | pause(1 by 1)
163 |
164 | repeat(2) {
165 | repeatingPattern()
166 | }
167 |
168 | note(1 by 12, A4)
169 | }
170 | }
171 |
--------------------------------------------------------------------------------
/core/src/commonMain/kotlin/it/krzeminski/fsynth/songs/SimpleDemoSong.kt:
--------------------------------------------------------------------------------
1 | package it.krzeminski.fsynth.songs
2 |
3 | import it.krzeminski.fsynth.instruments.organs
4 | import it.krzeminski.fsynth.types.MusicNote.* // ktlint-disable no-wildcard-imports
5 | import it.krzeminski.fsynth.types.by
6 | import it.krzeminski.fsynth.types.song
7 |
8 | // The "volume" parameter is needed to avoid generating values from outside <-1.0; 1.0>.
9 | // TODO: Handle this issue properly, maybe compute maximum volume automatically and scale appropriately.
10 | val simpleDemoSong = song(
11 | name = "Simple demo song",
12 | beatsPerMinute = 120) {
13 | track(instrument = organs, volume = 0.3f) {
14 | note(1 by 8, D4)
15 | note(1 by 16, Csharp4)
16 | note(1 by 16, D4)
17 | note(1 by 8, E4)
18 | note(1 by 8, D4)
19 |
20 | pause(1 by 8)
21 | chord(1 by 8, A3, D4, Fsharp4)
22 | chord(1 by 4, B3, D4, G4)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/core/src/commonMain/kotlin/it/krzeminski/fsynth/songs/SongRepository.kt:
--------------------------------------------------------------------------------
1 | package it.krzeminski.fsynth.songs
2 |
3 | // It has to be defined as a lazily initialized structure, not just a list.
4 | // There's a bug in Kotlin JS that for a regular list, can generate an incorrect JavaScript for initializing the list.
5 | // The order which Kotlin JS takes into account is defined by the names of the files where the objects are defined,
6 | // and in what order they appear when sorted alphabetically. Because of this, this very file appears before
7 | // 'VanHalenJumpIntro.kt', so 'vanHalenJumpIntro' constant was not yet defined, and in 'allSongs' list, the list
8 | // contained 'simpleDemoSong' and 'undefined'.
9 | // Tracked as https://youtrack.jetbrains.com/issue/KT-25796
10 | val allSongs by lazy {
11 | listOf(
12 | simpleDemoSong,
13 | vanHalenJumpIntro,
14 | pinkPantherThemeIntro)
15 | }
16 |
--------------------------------------------------------------------------------
/core/src/commonMain/kotlin/it/krzeminski/fsynth/songs/VanHalenJumpIntro.kt:
--------------------------------------------------------------------------------
1 | package it.krzeminski.fsynth.songs
2 |
3 | import it.krzeminski.fsynth.instruments.simpleDecayEnvelopeSynthesizer
4 | import it.krzeminski.fsynth.instruments.synthesizer
5 | import it.krzeminski.fsynth.types.MusicNote.* // ktlint-disable no-wildcard-imports
6 | import it.krzeminski.fsynth.types.by
7 | import it.krzeminski.fsynth.types.song
8 |
9 | val vanHalenJumpIntro = song(
10 | name = "Van Halen - Jump (intro)",
11 | beatsPerMinute = 133) {
12 | track(name = "Main", instrument = synthesizer, volume = 0.2f) {
13 | repeat(3) {
14 | pause(1 by 4)
15 | chord(1 by 8, G4, B4, D5)
16 | pause(1 by 4)
17 | chord(1 by 8, G4, C5, E5)
18 | pause(1 by 4)
19 | chord(1 by 8, F4, A4, C5)
20 | pause(1 by 4)
21 | chord(1 by 8, F4, A4, C5)
22 | pause(1 by 8)
23 | chord(1 by 8, G4, B4, D5)
24 | pause(1 by 8)
25 | chord(3 by 8, G4, B4, D5)
26 | chord(1 by 8, G4, C5, E5)
27 | pause(1 by 4)
28 | chord(1 by 8, F4, A4, C5)
29 | pause(1 by 8)
30 | chord(1 by 4, C4, F4, A4)
31 | chord(1 by 4, C4, E4, G4)
32 | chord(5 by 8, C4, D4, G4)
33 | }
34 |
35 | pause(1 by 4)
36 | repeat(4) {
37 | chord(1 by 8, F5, A5, C6)
38 | pause(1 by 4)
39 | }
40 | chord(1 by 4, C5, E5, G5)
41 | pause(1 by 4)
42 | chord(1 by 8, F4, A4, C5)
43 | pause(1 by 4)
44 | chord(1 by 8, F4, A4, C5)
45 | pause(1 by 8)
46 | chord(1 by 4, C4, F4, A4)
47 | chord(1 by 8, E4, G4)
48 | pause(1 by 8)
49 | chord(5 by 8, C4, D4, G4)
50 | }
51 |
52 | track(name = "Bass", instrument = simpleDecayEnvelopeSynthesizer, volume = 0.2f) {
53 | repeat(2) {
54 | chord(23 by 8, C2, C3)
55 | chord(1 by 2, F1, F2)
56 | chord(1 by 2, G1, G2)
57 | pause(1 by 8)
58 | }
59 |
60 | repeat(2) {
61 | repeat(23) {
62 | chord(1 by 16, C2, C3)
63 | pause(1 by 16)
64 | }
65 |
66 | repeat(4) {
67 | chord(1 by 16, F1, F2)
68 | pause(1 by 16)
69 | }
70 | repeat(5) {
71 | chord(1 by 16, G1, G2)
72 | pause(1 by 16)
73 | }
74 | }
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/core/src/commonMain/kotlin/it/krzeminski/fsynth/synthesis/Preprocessing.kt:
--------------------------------------------------------------------------------
1 | package it.krzeminski.fsynth.synthesis
2 |
3 | import it.krzeminski.fsynth.silence
4 | import it.krzeminski.fsynth.synthesis.types.SongForSynthesis
5 | import it.krzeminski.fsynth.synthesis.types.TrackForSynthesis
6 | import it.krzeminski.fsynth.types.BoundedWaveform
7 | import it.krzeminski.fsynth.types.NoteValue
8 | import it.krzeminski.fsynth.types.PositionedBoundedWaveform
9 | import it.krzeminski.fsynth.types.Song
10 | import it.krzeminski.fsynth.types.Track
11 | import it.krzeminski.fsynth.types.TrackSegment
12 | import it.krzeminski.fsynth.types.plus
13 | import it.krzeminski.fsynth.types.times
14 | import kotlin.math.ln
15 | import kotlin.math.pow
16 |
17 | fun Song.preprocessForSynthesis() = SongForSynthesis(tracks = tracks.preprocess(this))
18 |
19 | private fun List