diff --git a/.idea/misc.xml b/.idea/misc.xml index 97f8bf5..3d886c4 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,6 +11,8 @@ + + diff --git a/.idea/misc.xml b/.idea/misc.xml index 97f8bf5..3d886c4 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,6 +11,8 @@ + + diff --git a/app/build.gradle b/app/build.gradle index 35173c4..97ec2a0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -56,12 +56,12 @@ } dependencies { - implementation 'androidx.core:core-ktx:1.7.0' implementation 'androidx.appcompat:appcompat:1.3.0' implementation 'com.google.android.material:material:1.6.1' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'androidx.gridlayout:gridlayout:1.0.0' + implementation 'org.jetbrains.kotlin:kotlin-reflect:1.7.20-Beta' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' diff --git a/.idea/misc.xml b/.idea/misc.xml index 97f8bf5..3d886c4 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,6 +11,8 @@ + + diff --git a/app/build.gradle b/app/build.gradle index 35173c4..97ec2a0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -56,12 +56,12 @@ } dependencies { - implementation 'androidx.core:core-ktx:1.7.0' implementation 'androidx.appcompat:appcompat:1.3.0' implementation 'com.google.android.material:material:1.6.1' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'androidx.gridlayout:gridlayout:1.0.0' + implementation 'org.jetbrains.kotlin:kotlin-reflect:1.7.20-Beta' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index dc80bd1..8e98b44 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -15,6 +15,9 @@ } AudioHost *thiz = static_cast(userData); for (auto const &instrument: *thiz->instruments) { + if (!instrument) { + continue; + } instrument->render(buffer, sampleCount); } for (uint32_t i = 0; i < sampleCount; i++) { diff --git a/.idea/misc.xml b/.idea/misc.xml index 97f8bf5..3d886c4 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,6 +11,8 @@ + + diff --git a/app/build.gradle b/app/build.gradle index 35173c4..97ec2a0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -56,12 +56,12 @@ } dependencies { - implementation 'androidx.core:core-ktx:1.7.0' implementation 'androidx.appcompat:appcompat:1.3.0' implementation 'com.google.android.material:material:1.6.1' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'androidx.gridlayout:gridlayout:1.0.0' + implementation 'org.jetbrains.kotlin:kotlin-reflect:1.7.20-Beta' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index dc80bd1..8e98b44 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -15,6 +15,9 @@ } AudioHost *thiz = static_cast(userData); for (auto const &instrument: *thiz->instruments) { + if (!instrument) { + continue; + } instrument->render(buffer, sampleCount); } for (uint32_t i = 0; i < sampleCount; i++) { diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 6ebd2b0..5505a35 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -8,7 +8,7 @@ static AudioHost *audioHost; -template +template void *listGet(_InputIterator iterator, uint32_t n) { for (uint32_t i = 0; i < n; i++) { iterator++; @@ -16,6 +16,18 @@ return *iterator; } +Instrument *getInstrument(uint32_t id) { + return static_cast(listGet(audioHost->instruments->begin(), id)); +} + +template +void listSet(_InputIterator iterator, uint32_t n, void *value) { + for (uint32_t i = 0; i < n; i++) { + iterator++; + } + *iterator = static_cast(value); +} + extern "C" { JNIEXPORT void JNICALL @@ -23,7 +35,6 @@ audioHost = new AudioHost(); } -extern "C" JNIEXPORT jint JNICALL Java_com_lukas_music_instruments_InternalInstrument_createInstrument(JNIEnv *env, jobject thiz) { uint32_t result = audioHost->instruments->size(); @@ -32,43 +43,37 @@ return result; } -extern "C" -JNIEXPORT void JNICALL -Java_com_lukas_music_instruments_InternalInstrument_setInstrumentActive(JNIEnv *env, jobject thiz, - jint id, jboolean active) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->wave->amplitude = active ? 0.3 : 0.0; -} -} -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_startNote(JNIEnv *env, jobject thiz, jint id, jdouble frequency) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->startNote(frequency); + getInstrument(id)->startNote(frequency); } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_endNote(JNIEnv *env, jobject thiz, jint id) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->endNote(); + getInstrument(id)->endNote(); } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_ui_fragments_PlayFragment_setMasterVolume(JNIEnv *env, jobject thiz, jdouble volume) { audioHost->masterVolume = volume; } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setInstrumentWaveform(JNIEnv *env, jobject thiz, jint id, jint waveform) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->setWaveform(static_cast(waveform)); + getInstrument(id)->setWaveform(static_cast(waveform)); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, + jfloat volume) { + getInstrument(id)->wave->amplitude = volume; +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_destroy(JNIEnv *env, jobject thiz, jint id) { + listSet(audioHost->instruments->begin(), id, nullptr); + delete getInstrument(id); +} } \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 97f8bf5..3d886c4 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,6 +11,8 @@ + + diff --git a/app/build.gradle b/app/build.gradle index 35173c4..97ec2a0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -56,12 +56,12 @@ } dependencies { - implementation 'androidx.core:core-ktx:1.7.0' implementation 'androidx.appcompat:appcompat:1.3.0' implementation 'com.google.android.material:material:1.6.1' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'androidx.gridlayout:gridlayout:1.0.0' + implementation 'org.jetbrains.kotlin:kotlin-reflect:1.7.20-Beta' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index dc80bd1..8e98b44 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -15,6 +15,9 @@ } AudioHost *thiz = static_cast(userData); for (auto const &instrument: *thiz->instruments) { + if (!instrument) { + continue; + } instrument->render(buffer, sampleCount); } for (uint32_t i = 0; i < sampleCount; i++) { diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 6ebd2b0..5505a35 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -8,7 +8,7 @@ static AudioHost *audioHost; -template +template void *listGet(_InputIterator iterator, uint32_t n) { for (uint32_t i = 0; i < n; i++) { iterator++; @@ -16,6 +16,18 @@ return *iterator; } +Instrument *getInstrument(uint32_t id) { + return static_cast(listGet(audioHost->instruments->begin(), id)); +} + +template +void listSet(_InputIterator iterator, uint32_t n, void *value) { + for (uint32_t i = 0; i < n; i++) { + iterator++; + } + *iterator = static_cast(value); +} + extern "C" { JNIEXPORT void JNICALL @@ -23,7 +35,6 @@ audioHost = new AudioHost(); } -extern "C" JNIEXPORT jint JNICALL Java_com_lukas_music_instruments_InternalInstrument_createInstrument(JNIEnv *env, jobject thiz) { uint32_t result = audioHost->instruments->size(); @@ -32,43 +43,37 @@ return result; } -extern "C" -JNIEXPORT void JNICALL -Java_com_lukas_music_instruments_InternalInstrument_setInstrumentActive(JNIEnv *env, jobject thiz, - jint id, jboolean active) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->wave->amplitude = active ? 0.3 : 0.0; -} -} -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_startNote(JNIEnv *env, jobject thiz, jint id, jdouble frequency) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->startNote(frequency); + getInstrument(id)->startNote(frequency); } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_endNote(JNIEnv *env, jobject thiz, jint id) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->endNote(); + getInstrument(id)->endNote(); } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_ui_fragments_PlayFragment_setMasterVolume(JNIEnv *env, jobject thiz, jdouble volume) { audioHost->masterVolume = volume; } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setInstrumentWaveform(JNIEnv *env, jobject thiz, jint id, jint waveform) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->setWaveform(static_cast(waveform)); + getInstrument(id)->setWaveform(static_cast(waveform)); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, + jfloat volume) { + getInstrument(id)->wave->amplitude = volume; +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_destroy(JNIEnv *env, jobject thiz, jint id) { + listSet(audioHost->instruments->begin(), id, nullptr); + delete getInstrument(id); +} } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt new file mode 100644 index 0000000..56f21e6 --- /dev/null +++ b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2022 Lukas Eisenhauer + * + * This program 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, either version 3 of the License, or(at your option) any later version. + * + * This program 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 this program. If not, see . + */ + +package com.lukas.music + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TableRow +import androidx.core.view.setMargins +import androidx.fragment.app.DialogFragment +import com.google.android.material.button.MaterialButton +import com.lukas.music.databinding.FragmentEditVoiceBinding +import com.lukas.music.song.Song +import com.lukas.music.song.voice.Voice +import com.lukas.music.util.ArrayProperty +import com.lukas.music.util.setupToggle + +class EditVoiceFragment(private val voice: Voice) : DialogFragment() { + private lateinit var binding: FragmentEditVoiceBinding + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEditVoiceBinding.inflate(inflater) + for (row in 0 until voice.noteCount) { + val rowLayout = TableRow(binding.root.context) + for (column in 0 until Song.currentSong.beats) { + val button = MaterialButton(binding.root.context) + button.layoutParams = buttonLayout + button.setupToggle(ArrayProperty(voice.noteActive[column], row), R.color.blue) + rowLayout.addView(button) + } + binding.noteGrid.addView(rowLayout) + } + binding.noteGrid.isStretchAllColumns = true + binding.closeButton.setOnClickListener { + dismiss() + } + return binding.root + } + + override fun onStart() { + super.onStart() + dialog?.window?.setLayout( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + } + + companion object { + val buttonLayout = TableRow.LayoutParams(0, TableRow.LayoutParams.WRAP_CONTENT) + + init { + buttonLayout.setMargins(5) + } + } +} \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 97f8bf5..3d886c4 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,6 +11,8 @@ + + diff --git a/app/build.gradle b/app/build.gradle index 35173c4..97ec2a0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -56,12 +56,12 @@ } dependencies { - implementation 'androidx.core:core-ktx:1.7.0' implementation 'androidx.appcompat:appcompat:1.3.0' implementation 'com.google.android.material:material:1.6.1' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'androidx.gridlayout:gridlayout:1.0.0' + implementation 'org.jetbrains.kotlin:kotlin-reflect:1.7.20-Beta' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index dc80bd1..8e98b44 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -15,6 +15,9 @@ } AudioHost *thiz = static_cast(userData); for (auto const &instrument: *thiz->instruments) { + if (!instrument) { + continue; + } instrument->render(buffer, sampleCount); } for (uint32_t i = 0; i < sampleCount; i++) { diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 6ebd2b0..5505a35 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -8,7 +8,7 @@ static AudioHost *audioHost; -template +template void *listGet(_InputIterator iterator, uint32_t n) { for (uint32_t i = 0; i < n; i++) { iterator++; @@ -16,6 +16,18 @@ return *iterator; } +Instrument *getInstrument(uint32_t id) { + return static_cast(listGet(audioHost->instruments->begin(), id)); +} + +template +void listSet(_InputIterator iterator, uint32_t n, void *value) { + for (uint32_t i = 0; i < n; i++) { + iterator++; + } + *iterator = static_cast(value); +} + extern "C" { JNIEXPORT void JNICALL @@ -23,7 +35,6 @@ audioHost = new AudioHost(); } -extern "C" JNIEXPORT jint JNICALL Java_com_lukas_music_instruments_InternalInstrument_createInstrument(JNIEnv *env, jobject thiz) { uint32_t result = audioHost->instruments->size(); @@ -32,43 +43,37 @@ return result; } -extern "C" -JNIEXPORT void JNICALL -Java_com_lukas_music_instruments_InternalInstrument_setInstrumentActive(JNIEnv *env, jobject thiz, - jint id, jboolean active) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->wave->amplitude = active ? 0.3 : 0.0; -} -} -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_startNote(JNIEnv *env, jobject thiz, jint id, jdouble frequency) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->startNote(frequency); + getInstrument(id)->startNote(frequency); } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_endNote(JNIEnv *env, jobject thiz, jint id) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->endNote(); + getInstrument(id)->endNote(); } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_ui_fragments_PlayFragment_setMasterVolume(JNIEnv *env, jobject thiz, jdouble volume) { audioHost->masterVolume = volume; } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setInstrumentWaveform(JNIEnv *env, jobject thiz, jint id, jint waveform) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->setWaveform(static_cast(waveform)); + getInstrument(id)->setWaveform(static_cast(waveform)); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, + jfloat volume) { + getInstrument(id)->wave->amplitude = volume; +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_destroy(JNIEnv *env, jobject thiz, jint id) { + listSet(audioHost->instruments->begin(), id, nullptr); + delete getInstrument(id); +} } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt new file mode 100644 index 0000000..56f21e6 --- /dev/null +++ b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2022 Lukas Eisenhauer + * + * This program 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, either version 3 of the License, or(at your option) any later version. + * + * This program 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 this program. If not, see . + */ + +package com.lukas.music + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TableRow +import androidx.core.view.setMargins +import androidx.fragment.app.DialogFragment +import com.google.android.material.button.MaterialButton +import com.lukas.music.databinding.FragmentEditVoiceBinding +import com.lukas.music.song.Song +import com.lukas.music.song.voice.Voice +import com.lukas.music.util.ArrayProperty +import com.lukas.music.util.setupToggle + +class EditVoiceFragment(private val voice: Voice) : DialogFragment() { + private lateinit var binding: FragmentEditVoiceBinding + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEditVoiceBinding.inflate(inflater) + for (row in 0 until voice.noteCount) { + val rowLayout = TableRow(binding.root.context) + for (column in 0 until Song.currentSong.beats) { + val button = MaterialButton(binding.root.context) + button.layoutParams = buttonLayout + button.setupToggle(ArrayProperty(voice.noteActive[column], row), R.color.blue) + rowLayout.addView(button) + } + binding.noteGrid.addView(rowLayout) + } + binding.noteGrid.isStretchAllColumns = true + binding.closeButton.setOnClickListener { + dismiss() + } + return binding.root + } + + override fun onStart() { + super.onStart() + dialog?.window?.setLayout( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + } + + companion object { + val buttonLayout = TableRow.LayoutParams(0, TableRow.LayoutParams.WRAP_CONTENT) + + init { + buttonLayout.setMargins(5) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Instrument.kt b/app/src/main/java/com/lukas/music/instruments/Instrument.kt index 8ad90c6..92a896c 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -10,42 +10,22 @@ package com.lukas.music.instruments -import com.lukas.music.databinding.FragmentInstrumentBinding import com.lukas.music.song.note.Note import com.lukas.music.song.voice.BassVoice -import com.lukas.music.song.voice.ChordVoice import com.lukas.music.song.voice.Voice -abstract class Instrument(private var name: String) { - private var active = false +abstract class Instrument(var name: String) { + var voice: Voice = BassVoice(this) abstract var waveform: Waveform - - fun applyToView(binding: FragmentInstrumentBinding) { - binding.instrumentNameText.text = name - binding.editInstrumentButton.setOnClickListener { - println("click instrument $name") - } - binding.activeSwitch.setOnCheckedChangeListener { _, newActive -> - active = newActive - changeActive(newActive) - } - binding.activeSwitch.isChecked = active - } + abstract var volume: Float + abstract var muted: Boolean abstract fun startNote(note: Note) abstract fun stop() - abstract fun changeActive(newActive: Boolean) + abstract fun stopNote(note: Note) + abstract fun destroy() companion object { - val instruments = - mutableListOf( - MonoInstrument("Bass"), - PolyInstrument("Chords"), - ) - - val voice = mutableListOf( - BassVoice(instruments[0]), - ChordVoice(instruments[1]), - ) + val instruments = mutableListOf() } } \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 97f8bf5..3d886c4 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,6 +11,8 @@ + + diff --git a/app/build.gradle b/app/build.gradle index 35173c4..97ec2a0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -56,12 +56,12 @@ } dependencies { - implementation 'androidx.core:core-ktx:1.7.0' implementation 'androidx.appcompat:appcompat:1.3.0' implementation 'com.google.android.material:material:1.6.1' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'androidx.gridlayout:gridlayout:1.0.0' + implementation 'org.jetbrains.kotlin:kotlin-reflect:1.7.20-Beta' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index dc80bd1..8e98b44 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -15,6 +15,9 @@ } AudioHost *thiz = static_cast(userData); for (auto const &instrument: *thiz->instruments) { + if (!instrument) { + continue; + } instrument->render(buffer, sampleCount); } for (uint32_t i = 0; i < sampleCount; i++) { diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 6ebd2b0..5505a35 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -8,7 +8,7 @@ static AudioHost *audioHost; -template +template void *listGet(_InputIterator iterator, uint32_t n) { for (uint32_t i = 0; i < n; i++) { iterator++; @@ -16,6 +16,18 @@ return *iterator; } +Instrument *getInstrument(uint32_t id) { + return static_cast(listGet(audioHost->instruments->begin(), id)); +} + +template +void listSet(_InputIterator iterator, uint32_t n, void *value) { + for (uint32_t i = 0; i < n; i++) { + iterator++; + } + *iterator = static_cast(value); +} + extern "C" { JNIEXPORT void JNICALL @@ -23,7 +35,6 @@ audioHost = new AudioHost(); } -extern "C" JNIEXPORT jint JNICALL Java_com_lukas_music_instruments_InternalInstrument_createInstrument(JNIEnv *env, jobject thiz) { uint32_t result = audioHost->instruments->size(); @@ -32,43 +43,37 @@ return result; } -extern "C" -JNIEXPORT void JNICALL -Java_com_lukas_music_instruments_InternalInstrument_setInstrumentActive(JNIEnv *env, jobject thiz, - jint id, jboolean active) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->wave->amplitude = active ? 0.3 : 0.0; -} -} -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_startNote(JNIEnv *env, jobject thiz, jint id, jdouble frequency) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->startNote(frequency); + getInstrument(id)->startNote(frequency); } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_endNote(JNIEnv *env, jobject thiz, jint id) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->endNote(); + getInstrument(id)->endNote(); } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_ui_fragments_PlayFragment_setMasterVolume(JNIEnv *env, jobject thiz, jdouble volume) { audioHost->masterVolume = volume; } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setInstrumentWaveform(JNIEnv *env, jobject thiz, jint id, jint waveform) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->setWaveform(static_cast(waveform)); + getInstrument(id)->setWaveform(static_cast(waveform)); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, + jfloat volume) { + getInstrument(id)->wave->amplitude = volume; +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_destroy(JNIEnv *env, jobject thiz, jint id) { + listSet(audioHost->instruments->begin(), id, nullptr); + delete getInstrument(id); +} } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt new file mode 100644 index 0000000..56f21e6 --- /dev/null +++ b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2022 Lukas Eisenhauer + * + * This program 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, either version 3 of the License, or(at your option) any later version. + * + * This program 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 this program. If not, see . + */ + +package com.lukas.music + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TableRow +import androidx.core.view.setMargins +import androidx.fragment.app.DialogFragment +import com.google.android.material.button.MaterialButton +import com.lukas.music.databinding.FragmentEditVoiceBinding +import com.lukas.music.song.Song +import com.lukas.music.song.voice.Voice +import com.lukas.music.util.ArrayProperty +import com.lukas.music.util.setupToggle + +class EditVoiceFragment(private val voice: Voice) : DialogFragment() { + private lateinit var binding: FragmentEditVoiceBinding + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEditVoiceBinding.inflate(inflater) + for (row in 0 until voice.noteCount) { + val rowLayout = TableRow(binding.root.context) + for (column in 0 until Song.currentSong.beats) { + val button = MaterialButton(binding.root.context) + button.layoutParams = buttonLayout + button.setupToggle(ArrayProperty(voice.noteActive[column], row), R.color.blue) + rowLayout.addView(button) + } + binding.noteGrid.addView(rowLayout) + } + binding.noteGrid.isStretchAllColumns = true + binding.closeButton.setOnClickListener { + dismiss() + } + return binding.root + } + + override fun onStart() { + super.onStart() + dialog?.window?.setLayout( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + } + + companion object { + val buttonLayout = TableRow.LayoutParams(0, TableRow.LayoutParams.WRAP_CONTENT) + + init { + buttonLayout.setMargins(5) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Instrument.kt b/app/src/main/java/com/lukas/music/instruments/Instrument.kt index 8ad90c6..92a896c 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -10,42 +10,22 @@ package com.lukas.music.instruments -import com.lukas.music.databinding.FragmentInstrumentBinding import com.lukas.music.song.note.Note import com.lukas.music.song.voice.BassVoice -import com.lukas.music.song.voice.ChordVoice import com.lukas.music.song.voice.Voice -abstract class Instrument(private var name: String) { - private var active = false +abstract class Instrument(var name: String) { + var voice: Voice = BassVoice(this) abstract var waveform: Waveform - - fun applyToView(binding: FragmentInstrumentBinding) { - binding.instrumentNameText.text = name - binding.editInstrumentButton.setOnClickListener { - println("click instrument $name") - } - binding.activeSwitch.setOnCheckedChangeListener { _, newActive -> - active = newActive - changeActive(newActive) - } - binding.activeSwitch.isChecked = active - } + abstract var volume: Float + abstract var muted: Boolean abstract fun startNote(note: Note) abstract fun stop() - abstract fun changeActive(newActive: Boolean) + abstract fun stopNote(note: Note) + abstract fun destroy() companion object { - val instruments = - mutableListOf( - MonoInstrument("Bass"), - PolyInstrument("Chords"), - ) - - val voice = mutableListOf( - BassVoice(instruments[0]), - ChordVoice(instruments[1]), - ) + val instruments = mutableListOf() } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt index dc64bcc..b4c68a4 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -10,33 +10,67 @@ package com.lukas.music.instruments +import com.lukas.music.song.note.Note + class InternalInstrument { private val id = createInstrument() - var active: Boolean = false - set(value) { - field = value - setInstrumentActive(id, value) - } + var note: Note? = null var waveform: Waveform = Waveform.SINE set(value) { field = value setInstrumentWaveform(id, value.id) - // this is to resend the setInstrumentActive for the new waveform in the internal c++ code - active = active + refresh() } - fun startNote(frequency: Double) { - startNote(id, frequency) + var volume: Float = 0.3f + set(value) { + field = value + if (!muted) { + actualVolume = value + } + } + + var muted: Boolean = false + set(value) { + field = value + actualVolume = if (value) 0.0f else volume + } + + private var actualVolume: Float = 1.0f + set(value) { + field = value + setVolume(id, value) + } + + init { + refresh() + } + + private fun refresh() { + // this is to resend the old information to the internal c++ code (when changing the waveform) + muted = muted + volume = volume + } + + fun startNote(note: Note) { + this.note = note + startNote(id, note.frequency) } fun endNote() { + note = null endNote(id) } + fun destroy() { + destroy(id) + } + private external fun createInstrument(): Int - private external fun setInstrumentActive(id: Int, isActive: Boolean) private external fun setInstrumentWaveform(id: Int, waveform: Int) private external fun startNote(id: Int, frequency: Double) private external fun endNote(id: Int) + private external fun setVolume(id: Int, volume: Float) + private external fun destroy(id: Int) } \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 97f8bf5..3d886c4 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,6 +11,8 @@ + + diff --git a/app/build.gradle b/app/build.gradle index 35173c4..97ec2a0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -56,12 +56,12 @@ } dependencies { - implementation 'androidx.core:core-ktx:1.7.0' implementation 'androidx.appcompat:appcompat:1.3.0' implementation 'com.google.android.material:material:1.6.1' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'androidx.gridlayout:gridlayout:1.0.0' + implementation 'org.jetbrains.kotlin:kotlin-reflect:1.7.20-Beta' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index dc80bd1..8e98b44 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -15,6 +15,9 @@ } AudioHost *thiz = static_cast(userData); for (auto const &instrument: *thiz->instruments) { + if (!instrument) { + continue; + } instrument->render(buffer, sampleCount); } for (uint32_t i = 0; i < sampleCount; i++) { diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 6ebd2b0..5505a35 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -8,7 +8,7 @@ static AudioHost *audioHost; -template +template void *listGet(_InputIterator iterator, uint32_t n) { for (uint32_t i = 0; i < n; i++) { iterator++; @@ -16,6 +16,18 @@ return *iterator; } +Instrument *getInstrument(uint32_t id) { + return static_cast(listGet(audioHost->instruments->begin(), id)); +} + +template +void listSet(_InputIterator iterator, uint32_t n, void *value) { + for (uint32_t i = 0; i < n; i++) { + iterator++; + } + *iterator = static_cast(value); +} + extern "C" { JNIEXPORT void JNICALL @@ -23,7 +35,6 @@ audioHost = new AudioHost(); } -extern "C" JNIEXPORT jint JNICALL Java_com_lukas_music_instruments_InternalInstrument_createInstrument(JNIEnv *env, jobject thiz) { uint32_t result = audioHost->instruments->size(); @@ -32,43 +43,37 @@ return result; } -extern "C" -JNIEXPORT void JNICALL -Java_com_lukas_music_instruments_InternalInstrument_setInstrumentActive(JNIEnv *env, jobject thiz, - jint id, jboolean active) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->wave->amplitude = active ? 0.3 : 0.0; -} -} -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_startNote(JNIEnv *env, jobject thiz, jint id, jdouble frequency) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->startNote(frequency); + getInstrument(id)->startNote(frequency); } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_endNote(JNIEnv *env, jobject thiz, jint id) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->endNote(); + getInstrument(id)->endNote(); } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_ui_fragments_PlayFragment_setMasterVolume(JNIEnv *env, jobject thiz, jdouble volume) { audioHost->masterVolume = volume; } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setInstrumentWaveform(JNIEnv *env, jobject thiz, jint id, jint waveform) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->setWaveform(static_cast(waveform)); + getInstrument(id)->setWaveform(static_cast(waveform)); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, + jfloat volume) { + getInstrument(id)->wave->amplitude = volume; +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_destroy(JNIEnv *env, jobject thiz, jint id) { + listSet(audioHost->instruments->begin(), id, nullptr); + delete getInstrument(id); +} } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt new file mode 100644 index 0000000..56f21e6 --- /dev/null +++ b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2022 Lukas Eisenhauer + * + * This program 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, either version 3 of the License, or(at your option) any later version. + * + * This program 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 this program. If not, see . + */ + +package com.lukas.music + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TableRow +import androidx.core.view.setMargins +import androidx.fragment.app.DialogFragment +import com.google.android.material.button.MaterialButton +import com.lukas.music.databinding.FragmentEditVoiceBinding +import com.lukas.music.song.Song +import com.lukas.music.song.voice.Voice +import com.lukas.music.util.ArrayProperty +import com.lukas.music.util.setupToggle + +class EditVoiceFragment(private val voice: Voice) : DialogFragment() { + private lateinit var binding: FragmentEditVoiceBinding + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEditVoiceBinding.inflate(inflater) + for (row in 0 until voice.noteCount) { + val rowLayout = TableRow(binding.root.context) + for (column in 0 until Song.currentSong.beats) { + val button = MaterialButton(binding.root.context) + button.layoutParams = buttonLayout + button.setupToggle(ArrayProperty(voice.noteActive[column], row), R.color.blue) + rowLayout.addView(button) + } + binding.noteGrid.addView(rowLayout) + } + binding.noteGrid.isStretchAllColumns = true + binding.closeButton.setOnClickListener { + dismiss() + } + return binding.root + } + + override fun onStart() { + super.onStart() + dialog?.window?.setLayout( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + } + + companion object { + val buttonLayout = TableRow.LayoutParams(0, TableRow.LayoutParams.WRAP_CONTENT) + + init { + buttonLayout.setMargins(5) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Instrument.kt b/app/src/main/java/com/lukas/music/instruments/Instrument.kt index 8ad90c6..92a896c 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -10,42 +10,22 @@ package com.lukas.music.instruments -import com.lukas.music.databinding.FragmentInstrumentBinding import com.lukas.music.song.note.Note import com.lukas.music.song.voice.BassVoice -import com.lukas.music.song.voice.ChordVoice import com.lukas.music.song.voice.Voice -abstract class Instrument(private var name: String) { - private var active = false +abstract class Instrument(var name: String) { + var voice: Voice = BassVoice(this) abstract var waveform: Waveform - - fun applyToView(binding: FragmentInstrumentBinding) { - binding.instrumentNameText.text = name - binding.editInstrumentButton.setOnClickListener { - println("click instrument $name") - } - binding.activeSwitch.setOnCheckedChangeListener { _, newActive -> - active = newActive - changeActive(newActive) - } - binding.activeSwitch.isChecked = active - } + abstract var volume: Float + abstract var muted: Boolean abstract fun startNote(note: Note) abstract fun stop() - abstract fun changeActive(newActive: Boolean) + abstract fun stopNote(note: Note) + abstract fun destroy() companion object { - val instruments = - mutableListOf( - MonoInstrument("Bass"), - PolyInstrument("Chords"), - ) - - val voice = mutableListOf( - BassVoice(instruments[0]), - ChordVoice(instruments[1]), - ) + val instruments = mutableListOf() } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt index dc64bcc..b4c68a4 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -10,33 +10,67 @@ package com.lukas.music.instruments +import com.lukas.music.song.note.Note + class InternalInstrument { private val id = createInstrument() - var active: Boolean = false - set(value) { - field = value - setInstrumentActive(id, value) - } + var note: Note? = null var waveform: Waveform = Waveform.SINE set(value) { field = value setInstrumentWaveform(id, value.id) - // this is to resend the setInstrumentActive for the new waveform in the internal c++ code - active = active + refresh() } - fun startNote(frequency: Double) { - startNote(id, frequency) + var volume: Float = 0.3f + set(value) { + field = value + if (!muted) { + actualVolume = value + } + } + + var muted: Boolean = false + set(value) { + field = value + actualVolume = if (value) 0.0f else volume + } + + private var actualVolume: Float = 1.0f + set(value) { + field = value + setVolume(id, value) + } + + init { + refresh() + } + + private fun refresh() { + // this is to resend the old information to the internal c++ code (when changing the waveform) + muted = muted + volume = volume + } + + fun startNote(note: Note) { + this.note = note + startNote(id, note.frequency) } fun endNote() { + note = null endNote(id) } + fun destroy() { + destroy(id) + } + private external fun createInstrument(): Int - private external fun setInstrumentActive(id: Int, isActive: Boolean) private external fun setInstrumentWaveform(id: Int, waveform: Int) private external fun startNote(id: Int, frequency: Double) private external fun endNote(id: Int) + private external fun setVolume(id: Int, volume: Float) + private external fun destroy(id: Int) } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt index e7b7464..2b43524 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -21,15 +21,36 @@ internalInstrument.waveform = value } - override fun startNote(note: Note) { - internalInstrument.startNote(note.frequency) - } + override var volume: Float = 1.0f + set(value) { + field = value + internalInstrument.volume = volume + } - override fun changeActive(newActive: Boolean) { - internalInstrument.active = newActive + override var muted: Boolean = false + set(value) { + field = value + internalInstrument.muted = value + } + + override fun startNote(note: Note) { + if (note == internalInstrument.note) { + return + } + internalInstrument.startNote(note) } override fun stop() { internalInstrument.endNote() } + + override fun stopNote(note: Note) { + if (note == internalInstrument.note) { + stop() + } + } + + override fun destroy() { + internalInstrument.destroy() + } } \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 97f8bf5..3d886c4 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,6 +11,8 @@ + + diff --git a/app/build.gradle b/app/build.gradle index 35173c4..97ec2a0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -56,12 +56,12 @@ } dependencies { - implementation 'androidx.core:core-ktx:1.7.0' implementation 'androidx.appcompat:appcompat:1.3.0' implementation 'com.google.android.material:material:1.6.1' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'androidx.gridlayout:gridlayout:1.0.0' + implementation 'org.jetbrains.kotlin:kotlin-reflect:1.7.20-Beta' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index dc80bd1..8e98b44 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -15,6 +15,9 @@ } AudioHost *thiz = static_cast(userData); for (auto const &instrument: *thiz->instruments) { + if (!instrument) { + continue; + } instrument->render(buffer, sampleCount); } for (uint32_t i = 0; i < sampleCount; i++) { diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 6ebd2b0..5505a35 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -8,7 +8,7 @@ static AudioHost *audioHost; -template +template void *listGet(_InputIterator iterator, uint32_t n) { for (uint32_t i = 0; i < n; i++) { iterator++; @@ -16,6 +16,18 @@ return *iterator; } +Instrument *getInstrument(uint32_t id) { + return static_cast(listGet(audioHost->instruments->begin(), id)); +} + +template +void listSet(_InputIterator iterator, uint32_t n, void *value) { + for (uint32_t i = 0; i < n; i++) { + iterator++; + } + *iterator = static_cast(value); +} + extern "C" { JNIEXPORT void JNICALL @@ -23,7 +35,6 @@ audioHost = new AudioHost(); } -extern "C" JNIEXPORT jint JNICALL Java_com_lukas_music_instruments_InternalInstrument_createInstrument(JNIEnv *env, jobject thiz) { uint32_t result = audioHost->instruments->size(); @@ -32,43 +43,37 @@ return result; } -extern "C" -JNIEXPORT void JNICALL -Java_com_lukas_music_instruments_InternalInstrument_setInstrumentActive(JNIEnv *env, jobject thiz, - jint id, jboolean active) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->wave->amplitude = active ? 0.3 : 0.0; -} -} -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_startNote(JNIEnv *env, jobject thiz, jint id, jdouble frequency) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->startNote(frequency); + getInstrument(id)->startNote(frequency); } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_endNote(JNIEnv *env, jobject thiz, jint id) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->endNote(); + getInstrument(id)->endNote(); } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_ui_fragments_PlayFragment_setMasterVolume(JNIEnv *env, jobject thiz, jdouble volume) { audioHost->masterVolume = volume; } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setInstrumentWaveform(JNIEnv *env, jobject thiz, jint id, jint waveform) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->setWaveform(static_cast(waveform)); + getInstrument(id)->setWaveform(static_cast(waveform)); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, + jfloat volume) { + getInstrument(id)->wave->amplitude = volume; +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_destroy(JNIEnv *env, jobject thiz, jint id) { + listSet(audioHost->instruments->begin(), id, nullptr); + delete getInstrument(id); +} } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt new file mode 100644 index 0000000..56f21e6 --- /dev/null +++ b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2022 Lukas Eisenhauer + * + * This program 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, either version 3 of the License, or(at your option) any later version. + * + * This program 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 this program. If not, see . + */ + +package com.lukas.music + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TableRow +import androidx.core.view.setMargins +import androidx.fragment.app.DialogFragment +import com.google.android.material.button.MaterialButton +import com.lukas.music.databinding.FragmentEditVoiceBinding +import com.lukas.music.song.Song +import com.lukas.music.song.voice.Voice +import com.lukas.music.util.ArrayProperty +import com.lukas.music.util.setupToggle + +class EditVoiceFragment(private val voice: Voice) : DialogFragment() { + private lateinit var binding: FragmentEditVoiceBinding + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEditVoiceBinding.inflate(inflater) + for (row in 0 until voice.noteCount) { + val rowLayout = TableRow(binding.root.context) + for (column in 0 until Song.currentSong.beats) { + val button = MaterialButton(binding.root.context) + button.layoutParams = buttonLayout + button.setupToggle(ArrayProperty(voice.noteActive[column], row), R.color.blue) + rowLayout.addView(button) + } + binding.noteGrid.addView(rowLayout) + } + binding.noteGrid.isStretchAllColumns = true + binding.closeButton.setOnClickListener { + dismiss() + } + return binding.root + } + + override fun onStart() { + super.onStart() + dialog?.window?.setLayout( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + } + + companion object { + val buttonLayout = TableRow.LayoutParams(0, TableRow.LayoutParams.WRAP_CONTENT) + + init { + buttonLayout.setMargins(5) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Instrument.kt b/app/src/main/java/com/lukas/music/instruments/Instrument.kt index 8ad90c6..92a896c 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -10,42 +10,22 @@ package com.lukas.music.instruments -import com.lukas.music.databinding.FragmentInstrumentBinding import com.lukas.music.song.note.Note import com.lukas.music.song.voice.BassVoice -import com.lukas.music.song.voice.ChordVoice import com.lukas.music.song.voice.Voice -abstract class Instrument(private var name: String) { - private var active = false +abstract class Instrument(var name: String) { + var voice: Voice = BassVoice(this) abstract var waveform: Waveform - - fun applyToView(binding: FragmentInstrumentBinding) { - binding.instrumentNameText.text = name - binding.editInstrumentButton.setOnClickListener { - println("click instrument $name") - } - binding.activeSwitch.setOnCheckedChangeListener { _, newActive -> - active = newActive - changeActive(newActive) - } - binding.activeSwitch.isChecked = active - } + abstract var volume: Float + abstract var muted: Boolean abstract fun startNote(note: Note) abstract fun stop() - abstract fun changeActive(newActive: Boolean) + abstract fun stopNote(note: Note) + abstract fun destroy() companion object { - val instruments = - mutableListOf( - MonoInstrument("Bass"), - PolyInstrument("Chords"), - ) - - val voice = mutableListOf( - BassVoice(instruments[0]), - ChordVoice(instruments[1]), - ) + val instruments = mutableListOf() } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt index dc64bcc..b4c68a4 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -10,33 +10,67 @@ package com.lukas.music.instruments +import com.lukas.music.song.note.Note + class InternalInstrument { private val id = createInstrument() - var active: Boolean = false - set(value) { - field = value - setInstrumentActive(id, value) - } + var note: Note? = null var waveform: Waveform = Waveform.SINE set(value) { field = value setInstrumentWaveform(id, value.id) - // this is to resend the setInstrumentActive for the new waveform in the internal c++ code - active = active + refresh() } - fun startNote(frequency: Double) { - startNote(id, frequency) + var volume: Float = 0.3f + set(value) { + field = value + if (!muted) { + actualVolume = value + } + } + + var muted: Boolean = false + set(value) { + field = value + actualVolume = if (value) 0.0f else volume + } + + private var actualVolume: Float = 1.0f + set(value) { + field = value + setVolume(id, value) + } + + init { + refresh() + } + + private fun refresh() { + // this is to resend the old information to the internal c++ code (when changing the waveform) + muted = muted + volume = volume + } + + fun startNote(note: Note) { + this.note = note + startNote(id, note.frequency) } fun endNote() { + note = null endNote(id) } + fun destroy() { + destroy(id) + } + private external fun createInstrument(): Int - private external fun setInstrumentActive(id: Int, isActive: Boolean) private external fun setInstrumentWaveform(id: Int, waveform: Int) private external fun startNote(id: Int, frequency: Double) private external fun endNote(id: Int) + private external fun setVolume(id: Int, volume: Float) + private external fun destroy(id: Int) } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt index e7b7464..2b43524 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -21,15 +21,36 @@ internalInstrument.waveform = value } - override fun startNote(note: Note) { - internalInstrument.startNote(note.frequency) - } + override var volume: Float = 1.0f + set(value) { + field = value + internalInstrument.volume = volume + } - override fun changeActive(newActive: Boolean) { - internalInstrument.active = newActive + override var muted: Boolean = false + set(value) { + field = value + internalInstrument.muted = value + } + + override fun startNote(note: Note) { + if (note == internalInstrument.note) { + return + } + internalInstrument.startNote(note) } override fun stop() { internalInstrument.endNote() } + + override fun stopNote(note: Note) { + if (note == internalInstrument.note) { + stop() + } + } + + override fun destroy() { + internalInstrument.destroy() + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt index 9d8b4f9..26e4fbb 100644 --- a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt @@ -24,27 +24,55 @@ } } + override var volume: Float = 1.0f + set(value) { + field = value + for (internalInstrument in internalInstruments) { + internalInstrument.volume = volume + } + } + + override var muted: Boolean = false + set(value) { + field = value + for (instrument in internalInstruments) { + instrument.muted = value + } + } + override fun startNote(note: Note) { for ((index, instrumentPlaying) in playing.withIndex()) { if (!instrumentPlaying) { - internalInstruments[index].startNote(note.frequency) + internalInstruments[index].startNote(note) playing[index] = true return } + if (internalInstruments[index].note == note) { + return + } } throw IllegalStateException("cannot start another note with the current amount of oscillators") } - override fun changeActive(newActive: Boolean) { - for (instrument in internalInstruments) { - instrument.active = newActive - } - } - override fun stop() { for ((i, instrument) in internalInstruments.withIndex()) { instrument.endNote() playing[i] = false } } + + override fun stopNote(note: Note) { + for ((i, instrument) in internalInstruments.withIndex()) { + if (instrument.note == note) { + instrument.endNote() + playing[i] = false + } + } + } + + override fun destroy() { + for (instrument in internalInstruments) { + instrument.destroy() + } + } } \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 97f8bf5..3d886c4 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,6 +11,8 @@ + + diff --git a/app/build.gradle b/app/build.gradle index 35173c4..97ec2a0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -56,12 +56,12 @@ } dependencies { - implementation 'androidx.core:core-ktx:1.7.0' implementation 'androidx.appcompat:appcompat:1.3.0' implementation 'com.google.android.material:material:1.6.1' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'androidx.gridlayout:gridlayout:1.0.0' + implementation 'org.jetbrains.kotlin:kotlin-reflect:1.7.20-Beta' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index dc80bd1..8e98b44 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -15,6 +15,9 @@ } AudioHost *thiz = static_cast(userData); for (auto const &instrument: *thiz->instruments) { + if (!instrument) { + continue; + } instrument->render(buffer, sampleCount); } for (uint32_t i = 0; i < sampleCount; i++) { diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 6ebd2b0..5505a35 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -8,7 +8,7 @@ static AudioHost *audioHost; -template +template void *listGet(_InputIterator iterator, uint32_t n) { for (uint32_t i = 0; i < n; i++) { iterator++; @@ -16,6 +16,18 @@ return *iterator; } +Instrument *getInstrument(uint32_t id) { + return static_cast(listGet(audioHost->instruments->begin(), id)); +} + +template +void listSet(_InputIterator iterator, uint32_t n, void *value) { + for (uint32_t i = 0; i < n; i++) { + iterator++; + } + *iterator = static_cast(value); +} + extern "C" { JNIEXPORT void JNICALL @@ -23,7 +35,6 @@ audioHost = new AudioHost(); } -extern "C" JNIEXPORT jint JNICALL Java_com_lukas_music_instruments_InternalInstrument_createInstrument(JNIEnv *env, jobject thiz) { uint32_t result = audioHost->instruments->size(); @@ -32,43 +43,37 @@ return result; } -extern "C" -JNIEXPORT void JNICALL -Java_com_lukas_music_instruments_InternalInstrument_setInstrumentActive(JNIEnv *env, jobject thiz, - jint id, jboolean active) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->wave->amplitude = active ? 0.3 : 0.0; -} -} -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_startNote(JNIEnv *env, jobject thiz, jint id, jdouble frequency) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->startNote(frequency); + getInstrument(id)->startNote(frequency); } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_endNote(JNIEnv *env, jobject thiz, jint id) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->endNote(); + getInstrument(id)->endNote(); } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_ui_fragments_PlayFragment_setMasterVolume(JNIEnv *env, jobject thiz, jdouble volume) { audioHost->masterVolume = volume; } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setInstrumentWaveform(JNIEnv *env, jobject thiz, jint id, jint waveform) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->setWaveform(static_cast(waveform)); + getInstrument(id)->setWaveform(static_cast(waveform)); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, + jfloat volume) { + getInstrument(id)->wave->amplitude = volume; +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_destroy(JNIEnv *env, jobject thiz, jint id) { + listSet(audioHost->instruments->begin(), id, nullptr); + delete getInstrument(id); +} } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt new file mode 100644 index 0000000..56f21e6 --- /dev/null +++ b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2022 Lukas Eisenhauer + * + * This program 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, either version 3 of the License, or(at your option) any later version. + * + * This program 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 this program. If not, see . + */ + +package com.lukas.music + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TableRow +import androidx.core.view.setMargins +import androidx.fragment.app.DialogFragment +import com.google.android.material.button.MaterialButton +import com.lukas.music.databinding.FragmentEditVoiceBinding +import com.lukas.music.song.Song +import com.lukas.music.song.voice.Voice +import com.lukas.music.util.ArrayProperty +import com.lukas.music.util.setupToggle + +class EditVoiceFragment(private val voice: Voice) : DialogFragment() { + private lateinit var binding: FragmentEditVoiceBinding + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEditVoiceBinding.inflate(inflater) + for (row in 0 until voice.noteCount) { + val rowLayout = TableRow(binding.root.context) + for (column in 0 until Song.currentSong.beats) { + val button = MaterialButton(binding.root.context) + button.layoutParams = buttonLayout + button.setupToggle(ArrayProperty(voice.noteActive[column], row), R.color.blue) + rowLayout.addView(button) + } + binding.noteGrid.addView(rowLayout) + } + binding.noteGrid.isStretchAllColumns = true + binding.closeButton.setOnClickListener { + dismiss() + } + return binding.root + } + + override fun onStart() { + super.onStart() + dialog?.window?.setLayout( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + } + + companion object { + val buttonLayout = TableRow.LayoutParams(0, TableRow.LayoutParams.WRAP_CONTENT) + + init { + buttonLayout.setMargins(5) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Instrument.kt b/app/src/main/java/com/lukas/music/instruments/Instrument.kt index 8ad90c6..92a896c 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -10,42 +10,22 @@ package com.lukas.music.instruments -import com.lukas.music.databinding.FragmentInstrumentBinding import com.lukas.music.song.note.Note import com.lukas.music.song.voice.BassVoice -import com.lukas.music.song.voice.ChordVoice import com.lukas.music.song.voice.Voice -abstract class Instrument(private var name: String) { - private var active = false +abstract class Instrument(var name: String) { + var voice: Voice = BassVoice(this) abstract var waveform: Waveform - - fun applyToView(binding: FragmentInstrumentBinding) { - binding.instrumentNameText.text = name - binding.editInstrumentButton.setOnClickListener { - println("click instrument $name") - } - binding.activeSwitch.setOnCheckedChangeListener { _, newActive -> - active = newActive - changeActive(newActive) - } - binding.activeSwitch.isChecked = active - } + abstract var volume: Float + abstract var muted: Boolean abstract fun startNote(note: Note) abstract fun stop() - abstract fun changeActive(newActive: Boolean) + abstract fun stopNote(note: Note) + abstract fun destroy() companion object { - val instruments = - mutableListOf( - MonoInstrument("Bass"), - PolyInstrument("Chords"), - ) - - val voice = mutableListOf( - BassVoice(instruments[0]), - ChordVoice(instruments[1]), - ) + val instruments = mutableListOf() } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt index dc64bcc..b4c68a4 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -10,33 +10,67 @@ package com.lukas.music.instruments +import com.lukas.music.song.note.Note + class InternalInstrument { private val id = createInstrument() - var active: Boolean = false - set(value) { - field = value - setInstrumentActive(id, value) - } + var note: Note? = null var waveform: Waveform = Waveform.SINE set(value) { field = value setInstrumentWaveform(id, value.id) - // this is to resend the setInstrumentActive for the new waveform in the internal c++ code - active = active + refresh() } - fun startNote(frequency: Double) { - startNote(id, frequency) + var volume: Float = 0.3f + set(value) { + field = value + if (!muted) { + actualVolume = value + } + } + + var muted: Boolean = false + set(value) { + field = value + actualVolume = if (value) 0.0f else volume + } + + private var actualVolume: Float = 1.0f + set(value) { + field = value + setVolume(id, value) + } + + init { + refresh() + } + + private fun refresh() { + // this is to resend the old information to the internal c++ code (when changing the waveform) + muted = muted + volume = volume + } + + fun startNote(note: Note) { + this.note = note + startNote(id, note.frequency) } fun endNote() { + note = null endNote(id) } + fun destroy() { + destroy(id) + } + private external fun createInstrument(): Int - private external fun setInstrumentActive(id: Int, isActive: Boolean) private external fun setInstrumentWaveform(id: Int, waveform: Int) private external fun startNote(id: Int, frequency: Double) private external fun endNote(id: Int) + private external fun setVolume(id: Int, volume: Float) + private external fun destroy(id: Int) } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt index e7b7464..2b43524 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -21,15 +21,36 @@ internalInstrument.waveform = value } - override fun startNote(note: Note) { - internalInstrument.startNote(note.frequency) - } + override var volume: Float = 1.0f + set(value) { + field = value + internalInstrument.volume = volume + } - override fun changeActive(newActive: Boolean) { - internalInstrument.active = newActive + override var muted: Boolean = false + set(value) { + field = value + internalInstrument.muted = value + } + + override fun startNote(note: Note) { + if (note == internalInstrument.note) { + return + } + internalInstrument.startNote(note) } override fun stop() { internalInstrument.endNote() } + + override fun stopNote(note: Note) { + if (note == internalInstrument.note) { + stop() + } + } + + override fun destroy() { + internalInstrument.destroy() + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt index 9d8b4f9..26e4fbb 100644 --- a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt @@ -24,27 +24,55 @@ } } + override var volume: Float = 1.0f + set(value) { + field = value + for (internalInstrument in internalInstruments) { + internalInstrument.volume = volume + } + } + + override var muted: Boolean = false + set(value) { + field = value + for (instrument in internalInstruments) { + instrument.muted = value + } + } + override fun startNote(note: Note) { for ((index, instrumentPlaying) in playing.withIndex()) { if (!instrumentPlaying) { - internalInstruments[index].startNote(note.frequency) + internalInstruments[index].startNote(note) playing[index] = true return } + if (internalInstruments[index].note == note) { + return + } } throw IllegalStateException("cannot start another note with the current amount of oscillators") } - override fun changeActive(newActive: Boolean) { - for (instrument in internalInstruments) { - instrument.active = newActive - } - } - override fun stop() { for ((i, instrument) in internalInstruments.withIndex()) { instrument.endNote() playing[i] = false } } + + override fun stopNote(note: Note) { + for ((i, instrument) in internalInstruments.withIndex()) { + if (instrument.note == note) { + instrument.endNote() + playing[i] = false + } + } + } + + override fun destroy() { + for (instrument in internalInstruments) { + instrument.destroy() + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/Song.kt b/app/src/main/java/com/lukas/music/song/Song.kt index a3123f2..fc5421b 100644 --- a/app/src/main/java/com/lukas/music/song/Song.kt +++ b/app/src/main/java/com/lukas/music/song/Song.kt @@ -16,10 +16,33 @@ import com.lukas.music.util.Cycle class Song( - var root: Note, + root: Note, val beats: Int ) : Cycle(beats) { val chordProgression = ChordProgression() + var soloInstrument: Instrument? = null + set(value) { + field = value + value?.let { + for (instrument in Instrument.instruments) { + if (instrument != value) { + instrument.stop() + } + } + } + } + + var root: Note = root + set(value) { + field = value + stopAllInstruments() + } + + private fun stopAllInstruments() { + for (instrument in Instrument.instruments) { + instrument.stop() + } + } init { for (i in 0 until beats) { @@ -27,6 +50,7 @@ } wraparoundListeners += { chordProgression.step() + stopAllInstruments() } } @@ -34,8 +58,12 @@ super.step() val chord = chordProgression.currentItem?.currentItem ?: return index val chordNotes = chord.getNotes(root) - for (voice in Instrument.voice) { - voice.step(root, chordNotes) + soloInstrument?.let { + it.voice.step(root, chordNotes, index) + } ?: run { + for (instrument in Instrument.instruments) { + instrument.voice.step(root, chordNotes, index) + } } return index } diff --git a/.idea/misc.xml b/.idea/misc.xml index 97f8bf5..3d886c4 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,6 +11,8 @@ + + diff --git a/app/build.gradle b/app/build.gradle index 35173c4..97ec2a0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -56,12 +56,12 @@ } dependencies { - implementation 'androidx.core:core-ktx:1.7.0' implementation 'androidx.appcompat:appcompat:1.3.0' implementation 'com.google.android.material:material:1.6.1' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'androidx.gridlayout:gridlayout:1.0.0' + implementation 'org.jetbrains.kotlin:kotlin-reflect:1.7.20-Beta' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index dc80bd1..8e98b44 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -15,6 +15,9 @@ } AudioHost *thiz = static_cast(userData); for (auto const &instrument: *thiz->instruments) { + if (!instrument) { + continue; + } instrument->render(buffer, sampleCount); } for (uint32_t i = 0; i < sampleCount; i++) { diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 6ebd2b0..5505a35 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -8,7 +8,7 @@ static AudioHost *audioHost; -template +template void *listGet(_InputIterator iterator, uint32_t n) { for (uint32_t i = 0; i < n; i++) { iterator++; @@ -16,6 +16,18 @@ return *iterator; } +Instrument *getInstrument(uint32_t id) { + return static_cast(listGet(audioHost->instruments->begin(), id)); +} + +template +void listSet(_InputIterator iterator, uint32_t n, void *value) { + for (uint32_t i = 0; i < n; i++) { + iterator++; + } + *iterator = static_cast(value); +} + extern "C" { JNIEXPORT void JNICALL @@ -23,7 +35,6 @@ audioHost = new AudioHost(); } -extern "C" JNIEXPORT jint JNICALL Java_com_lukas_music_instruments_InternalInstrument_createInstrument(JNIEnv *env, jobject thiz) { uint32_t result = audioHost->instruments->size(); @@ -32,43 +43,37 @@ return result; } -extern "C" -JNIEXPORT void JNICALL -Java_com_lukas_music_instruments_InternalInstrument_setInstrumentActive(JNIEnv *env, jobject thiz, - jint id, jboolean active) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->wave->amplitude = active ? 0.3 : 0.0; -} -} -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_startNote(JNIEnv *env, jobject thiz, jint id, jdouble frequency) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->startNote(frequency); + getInstrument(id)->startNote(frequency); } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_endNote(JNIEnv *env, jobject thiz, jint id) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->endNote(); + getInstrument(id)->endNote(); } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_ui_fragments_PlayFragment_setMasterVolume(JNIEnv *env, jobject thiz, jdouble volume) { audioHost->masterVolume = volume; } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setInstrumentWaveform(JNIEnv *env, jobject thiz, jint id, jint waveform) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->setWaveform(static_cast(waveform)); + getInstrument(id)->setWaveform(static_cast(waveform)); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, + jfloat volume) { + getInstrument(id)->wave->amplitude = volume; +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_destroy(JNIEnv *env, jobject thiz, jint id) { + listSet(audioHost->instruments->begin(), id, nullptr); + delete getInstrument(id); +} } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt new file mode 100644 index 0000000..56f21e6 --- /dev/null +++ b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2022 Lukas Eisenhauer + * + * This program 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, either version 3 of the License, or(at your option) any later version. + * + * This program 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 this program. If not, see . + */ + +package com.lukas.music + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TableRow +import androidx.core.view.setMargins +import androidx.fragment.app.DialogFragment +import com.google.android.material.button.MaterialButton +import com.lukas.music.databinding.FragmentEditVoiceBinding +import com.lukas.music.song.Song +import com.lukas.music.song.voice.Voice +import com.lukas.music.util.ArrayProperty +import com.lukas.music.util.setupToggle + +class EditVoiceFragment(private val voice: Voice) : DialogFragment() { + private lateinit var binding: FragmentEditVoiceBinding + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEditVoiceBinding.inflate(inflater) + for (row in 0 until voice.noteCount) { + val rowLayout = TableRow(binding.root.context) + for (column in 0 until Song.currentSong.beats) { + val button = MaterialButton(binding.root.context) + button.layoutParams = buttonLayout + button.setupToggle(ArrayProperty(voice.noteActive[column], row), R.color.blue) + rowLayout.addView(button) + } + binding.noteGrid.addView(rowLayout) + } + binding.noteGrid.isStretchAllColumns = true + binding.closeButton.setOnClickListener { + dismiss() + } + return binding.root + } + + override fun onStart() { + super.onStart() + dialog?.window?.setLayout( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + } + + companion object { + val buttonLayout = TableRow.LayoutParams(0, TableRow.LayoutParams.WRAP_CONTENT) + + init { + buttonLayout.setMargins(5) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Instrument.kt b/app/src/main/java/com/lukas/music/instruments/Instrument.kt index 8ad90c6..92a896c 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -10,42 +10,22 @@ package com.lukas.music.instruments -import com.lukas.music.databinding.FragmentInstrumentBinding import com.lukas.music.song.note.Note import com.lukas.music.song.voice.BassVoice -import com.lukas.music.song.voice.ChordVoice import com.lukas.music.song.voice.Voice -abstract class Instrument(private var name: String) { - private var active = false +abstract class Instrument(var name: String) { + var voice: Voice = BassVoice(this) abstract var waveform: Waveform - - fun applyToView(binding: FragmentInstrumentBinding) { - binding.instrumentNameText.text = name - binding.editInstrumentButton.setOnClickListener { - println("click instrument $name") - } - binding.activeSwitch.setOnCheckedChangeListener { _, newActive -> - active = newActive - changeActive(newActive) - } - binding.activeSwitch.isChecked = active - } + abstract var volume: Float + abstract var muted: Boolean abstract fun startNote(note: Note) abstract fun stop() - abstract fun changeActive(newActive: Boolean) + abstract fun stopNote(note: Note) + abstract fun destroy() companion object { - val instruments = - mutableListOf( - MonoInstrument("Bass"), - PolyInstrument("Chords"), - ) - - val voice = mutableListOf( - BassVoice(instruments[0]), - ChordVoice(instruments[1]), - ) + val instruments = mutableListOf() } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt index dc64bcc..b4c68a4 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -10,33 +10,67 @@ package com.lukas.music.instruments +import com.lukas.music.song.note.Note + class InternalInstrument { private val id = createInstrument() - var active: Boolean = false - set(value) { - field = value - setInstrumentActive(id, value) - } + var note: Note? = null var waveform: Waveform = Waveform.SINE set(value) { field = value setInstrumentWaveform(id, value.id) - // this is to resend the setInstrumentActive for the new waveform in the internal c++ code - active = active + refresh() } - fun startNote(frequency: Double) { - startNote(id, frequency) + var volume: Float = 0.3f + set(value) { + field = value + if (!muted) { + actualVolume = value + } + } + + var muted: Boolean = false + set(value) { + field = value + actualVolume = if (value) 0.0f else volume + } + + private var actualVolume: Float = 1.0f + set(value) { + field = value + setVolume(id, value) + } + + init { + refresh() + } + + private fun refresh() { + // this is to resend the old information to the internal c++ code (when changing the waveform) + muted = muted + volume = volume + } + + fun startNote(note: Note) { + this.note = note + startNote(id, note.frequency) } fun endNote() { + note = null endNote(id) } + fun destroy() { + destroy(id) + } + private external fun createInstrument(): Int - private external fun setInstrumentActive(id: Int, isActive: Boolean) private external fun setInstrumentWaveform(id: Int, waveform: Int) private external fun startNote(id: Int, frequency: Double) private external fun endNote(id: Int) + private external fun setVolume(id: Int, volume: Float) + private external fun destroy(id: Int) } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt index e7b7464..2b43524 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -21,15 +21,36 @@ internalInstrument.waveform = value } - override fun startNote(note: Note) { - internalInstrument.startNote(note.frequency) - } + override var volume: Float = 1.0f + set(value) { + field = value + internalInstrument.volume = volume + } - override fun changeActive(newActive: Boolean) { - internalInstrument.active = newActive + override var muted: Boolean = false + set(value) { + field = value + internalInstrument.muted = value + } + + override fun startNote(note: Note) { + if (note == internalInstrument.note) { + return + } + internalInstrument.startNote(note) } override fun stop() { internalInstrument.endNote() } + + override fun stopNote(note: Note) { + if (note == internalInstrument.note) { + stop() + } + } + + override fun destroy() { + internalInstrument.destroy() + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt index 9d8b4f9..26e4fbb 100644 --- a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt @@ -24,27 +24,55 @@ } } + override var volume: Float = 1.0f + set(value) { + field = value + for (internalInstrument in internalInstruments) { + internalInstrument.volume = volume + } + } + + override var muted: Boolean = false + set(value) { + field = value + for (instrument in internalInstruments) { + instrument.muted = value + } + } + override fun startNote(note: Note) { for ((index, instrumentPlaying) in playing.withIndex()) { if (!instrumentPlaying) { - internalInstruments[index].startNote(note.frequency) + internalInstruments[index].startNote(note) playing[index] = true return } + if (internalInstruments[index].note == note) { + return + } } throw IllegalStateException("cannot start another note with the current amount of oscillators") } - override fun changeActive(newActive: Boolean) { - for (instrument in internalInstruments) { - instrument.active = newActive - } - } - override fun stop() { for ((i, instrument) in internalInstruments.withIndex()) { instrument.endNote() playing[i] = false } } + + override fun stopNote(note: Note) { + for ((i, instrument) in internalInstruments.withIndex()) { + if (instrument.note == note) { + instrument.endNote() + playing[i] = false + } + } + } + + override fun destroy() { + for (instrument in internalInstruments) { + instrument.destroy() + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/Song.kt b/app/src/main/java/com/lukas/music/song/Song.kt index a3123f2..fc5421b 100644 --- a/app/src/main/java/com/lukas/music/song/Song.kt +++ b/app/src/main/java/com/lukas/music/song/Song.kt @@ -16,10 +16,33 @@ import com.lukas.music.util.Cycle class Song( - var root: Note, + root: Note, val beats: Int ) : Cycle(beats) { val chordProgression = ChordProgression() + var soloInstrument: Instrument? = null + set(value) { + field = value + value?.let { + for (instrument in Instrument.instruments) { + if (instrument != value) { + instrument.stop() + } + } + } + } + + var root: Note = root + set(value) { + field = value + stopAllInstruments() + } + + private fun stopAllInstruments() { + for (instrument in Instrument.instruments) { + instrument.stop() + } + } init { for (i in 0 until beats) { @@ -27,6 +50,7 @@ } wraparoundListeners += { chordProgression.step() + stopAllInstruments() } } @@ -34,8 +58,12 @@ super.step() val chord = chordProgression.currentItem?.currentItem ?: return index val chordNotes = chord.getNotes(root) - for (voice in Instrument.voice) { - voice.step(root, chordNotes) + soloInstrument?.let { + it.voice.step(root, chordNotes, index) + } ?: run { + for (instrument in Instrument.instruments) { + instrument.voice.step(root, chordNotes, index) + } } return index } diff --git a/app/src/main/java/com/lukas/music/song/chords/Chord.kt b/app/src/main/java/com/lukas/music/song/chords/Chord.kt index db45fdd..4400dae 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Chord.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Chord.kt @@ -19,6 +19,12 @@ interval = Interval(value) } var interval = Interval(note) + set(value) { + field = value + if (note != value.distance) { + note = value.distance + } + } fun getNotes(root: Note): Array { return Array(chordType.notes.size) { root + note + chordType.notes[it] } diff --git a/.idea/misc.xml b/.idea/misc.xml index 97f8bf5..3d886c4 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,6 +11,8 @@ + + diff --git a/app/build.gradle b/app/build.gradle index 35173c4..97ec2a0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -56,12 +56,12 @@ } dependencies { - implementation 'androidx.core:core-ktx:1.7.0' implementation 'androidx.appcompat:appcompat:1.3.0' implementation 'com.google.android.material:material:1.6.1' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'androidx.gridlayout:gridlayout:1.0.0' + implementation 'org.jetbrains.kotlin:kotlin-reflect:1.7.20-Beta' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index dc80bd1..8e98b44 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -15,6 +15,9 @@ } AudioHost *thiz = static_cast(userData); for (auto const &instrument: *thiz->instruments) { + if (!instrument) { + continue; + } instrument->render(buffer, sampleCount); } for (uint32_t i = 0; i < sampleCount; i++) { diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 6ebd2b0..5505a35 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -8,7 +8,7 @@ static AudioHost *audioHost; -template +template void *listGet(_InputIterator iterator, uint32_t n) { for (uint32_t i = 0; i < n; i++) { iterator++; @@ -16,6 +16,18 @@ return *iterator; } +Instrument *getInstrument(uint32_t id) { + return static_cast(listGet(audioHost->instruments->begin(), id)); +} + +template +void listSet(_InputIterator iterator, uint32_t n, void *value) { + for (uint32_t i = 0; i < n; i++) { + iterator++; + } + *iterator = static_cast(value); +} + extern "C" { JNIEXPORT void JNICALL @@ -23,7 +35,6 @@ audioHost = new AudioHost(); } -extern "C" JNIEXPORT jint JNICALL Java_com_lukas_music_instruments_InternalInstrument_createInstrument(JNIEnv *env, jobject thiz) { uint32_t result = audioHost->instruments->size(); @@ -32,43 +43,37 @@ return result; } -extern "C" -JNIEXPORT void JNICALL -Java_com_lukas_music_instruments_InternalInstrument_setInstrumentActive(JNIEnv *env, jobject thiz, - jint id, jboolean active) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->wave->amplitude = active ? 0.3 : 0.0; -} -} -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_startNote(JNIEnv *env, jobject thiz, jint id, jdouble frequency) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->startNote(frequency); + getInstrument(id)->startNote(frequency); } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_endNote(JNIEnv *env, jobject thiz, jint id) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->endNote(); + getInstrument(id)->endNote(); } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_ui_fragments_PlayFragment_setMasterVolume(JNIEnv *env, jobject thiz, jdouble volume) { audioHost->masterVolume = volume; } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setInstrumentWaveform(JNIEnv *env, jobject thiz, jint id, jint waveform) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->setWaveform(static_cast(waveform)); + getInstrument(id)->setWaveform(static_cast(waveform)); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, + jfloat volume) { + getInstrument(id)->wave->amplitude = volume; +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_destroy(JNIEnv *env, jobject thiz, jint id) { + listSet(audioHost->instruments->begin(), id, nullptr); + delete getInstrument(id); +} } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt new file mode 100644 index 0000000..56f21e6 --- /dev/null +++ b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2022 Lukas Eisenhauer + * + * This program 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, either version 3 of the License, or(at your option) any later version. + * + * This program 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 this program. If not, see . + */ + +package com.lukas.music + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TableRow +import androidx.core.view.setMargins +import androidx.fragment.app.DialogFragment +import com.google.android.material.button.MaterialButton +import com.lukas.music.databinding.FragmentEditVoiceBinding +import com.lukas.music.song.Song +import com.lukas.music.song.voice.Voice +import com.lukas.music.util.ArrayProperty +import com.lukas.music.util.setupToggle + +class EditVoiceFragment(private val voice: Voice) : DialogFragment() { + private lateinit var binding: FragmentEditVoiceBinding + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEditVoiceBinding.inflate(inflater) + for (row in 0 until voice.noteCount) { + val rowLayout = TableRow(binding.root.context) + for (column in 0 until Song.currentSong.beats) { + val button = MaterialButton(binding.root.context) + button.layoutParams = buttonLayout + button.setupToggle(ArrayProperty(voice.noteActive[column], row), R.color.blue) + rowLayout.addView(button) + } + binding.noteGrid.addView(rowLayout) + } + binding.noteGrid.isStretchAllColumns = true + binding.closeButton.setOnClickListener { + dismiss() + } + return binding.root + } + + override fun onStart() { + super.onStart() + dialog?.window?.setLayout( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + } + + companion object { + val buttonLayout = TableRow.LayoutParams(0, TableRow.LayoutParams.WRAP_CONTENT) + + init { + buttonLayout.setMargins(5) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Instrument.kt b/app/src/main/java/com/lukas/music/instruments/Instrument.kt index 8ad90c6..92a896c 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -10,42 +10,22 @@ package com.lukas.music.instruments -import com.lukas.music.databinding.FragmentInstrumentBinding import com.lukas.music.song.note.Note import com.lukas.music.song.voice.BassVoice -import com.lukas.music.song.voice.ChordVoice import com.lukas.music.song.voice.Voice -abstract class Instrument(private var name: String) { - private var active = false +abstract class Instrument(var name: String) { + var voice: Voice = BassVoice(this) abstract var waveform: Waveform - - fun applyToView(binding: FragmentInstrumentBinding) { - binding.instrumentNameText.text = name - binding.editInstrumentButton.setOnClickListener { - println("click instrument $name") - } - binding.activeSwitch.setOnCheckedChangeListener { _, newActive -> - active = newActive - changeActive(newActive) - } - binding.activeSwitch.isChecked = active - } + abstract var volume: Float + abstract var muted: Boolean abstract fun startNote(note: Note) abstract fun stop() - abstract fun changeActive(newActive: Boolean) + abstract fun stopNote(note: Note) + abstract fun destroy() companion object { - val instruments = - mutableListOf( - MonoInstrument("Bass"), - PolyInstrument("Chords"), - ) - - val voice = mutableListOf( - BassVoice(instruments[0]), - ChordVoice(instruments[1]), - ) + val instruments = mutableListOf() } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt index dc64bcc..b4c68a4 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -10,33 +10,67 @@ package com.lukas.music.instruments +import com.lukas.music.song.note.Note + class InternalInstrument { private val id = createInstrument() - var active: Boolean = false - set(value) { - field = value - setInstrumentActive(id, value) - } + var note: Note? = null var waveform: Waveform = Waveform.SINE set(value) { field = value setInstrumentWaveform(id, value.id) - // this is to resend the setInstrumentActive for the new waveform in the internal c++ code - active = active + refresh() } - fun startNote(frequency: Double) { - startNote(id, frequency) + var volume: Float = 0.3f + set(value) { + field = value + if (!muted) { + actualVolume = value + } + } + + var muted: Boolean = false + set(value) { + field = value + actualVolume = if (value) 0.0f else volume + } + + private var actualVolume: Float = 1.0f + set(value) { + field = value + setVolume(id, value) + } + + init { + refresh() + } + + private fun refresh() { + // this is to resend the old information to the internal c++ code (when changing the waveform) + muted = muted + volume = volume + } + + fun startNote(note: Note) { + this.note = note + startNote(id, note.frequency) } fun endNote() { + note = null endNote(id) } + fun destroy() { + destroy(id) + } + private external fun createInstrument(): Int - private external fun setInstrumentActive(id: Int, isActive: Boolean) private external fun setInstrumentWaveform(id: Int, waveform: Int) private external fun startNote(id: Int, frequency: Double) private external fun endNote(id: Int) + private external fun setVolume(id: Int, volume: Float) + private external fun destroy(id: Int) } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt index e7b7464..2b43524 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -21,15 +21,36 @@ internalInstrument.waveform = value } - override fun startNote(note: Note) { - internalInstrument.startNote(note.frequency) - } + override var volume: Float = 1.0f + set(value) { + field = value + internalInstrument.volume = volume + } - override fun changeActive(newActive: Boolean) { - internalInstrument.active = newActive + override var muted: Boolean = false + set(value) { + field = value + internalInstrument.muted = value + } + + override fun startNote(note: Note) { + if (note == internalInstrument.note) { + return + } + internalInstrument.startNote(note) } override fun stop() { internalInstrument.endNote() } + + override fun stopNote(note: Note) { + if (note == internalInstrument.note) { + stop() + } + } + + override fun destroy() { + internalInstrument.destroy() + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt index 9d8b4f9..26e4fbb 100644 --- a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt @@ -24,27 +24,55 @@ } } + override var volume: Float = 1.0f + set(value) { + field = value + for (internalInstrument in internalInstruments) { + internalInstrument.volume = volume + } + } + + override var muted: Boolean = false + set(value) { + field = value + for (instrument in internalInstruments) { + instrument.muted = value + } + } + override fun startNote(note: Note) { for ((index, instrumentPlaying) in playing.withIndex()) { if (!instrumentPlaying) { - internalInstruments[index].startNote(note.frequency) + internalInstruments[index].startNote(note) playing[index] = true return } + if (internalInstruments[index].note == note) { + return + } } throw IllegalStateException("cannot start another note with the current amount of oscillators") } - override fun changeActive(newActive: Boolean) { - for (instrument in internalInstruments) { - instrument.active = newActive - } - } - override fun stop() { for ((i, instrument) in internalInstruments.withIndex()) { instrument.endNote() playing[i] = false } } + + override fun stopNote(note: Note) { + for ((i, instrument) in internalInstruments.withIndex()) { + if (instrument.note == note) { + instrument.endNote() + playing[i] = false + } + } + } + + override fun destroy() { + for (instrument in internalInstruments) { + instrument.destroy() + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/Song.kt b/app/src/main/java/com/lukas/music/song/Song.kt index a3123f2..fc5421b 100644 --- a/app/src/main/java/com/lukas/music/song/Song.kt +++ b/app/src/main/java/com/lukas/music/song/Song.kt @@ -16,10 +16,33 @@ import com.lukas.music.util.Cycle class Song( - var root: Note, + root: Note, val beats: Int ) : Cycle(beats) { val chordProgression = ChordProgression() + var soloInstrument: Instrument? = null + set(value) { + field = value + value?.let { + for (instrument in Instrument.instruments) { + if (instrument != value) { + instrument.stop() + } + } + } + } + + var root: Note = root + set(value) { + field = value + stopAllInstruments() + } + + private fun stopAllInstruments() { + for (instrument in Instrument.instruments) { + instrument.stop() + } + } init { for (i in 0 until beats) { @@ -27,6 +50,7 @@ } wraparoundListeners += { chordProgression.step() + stopAllInstruments() } } @@ -34,8 +58,12 @@ super.step() val chord = chordProgression.currentItem?.currentItem ?: return index val chordNotes = chord.getNotes(root) - for (voice in Instrument.voice) { - voice.step(root, chordNotes) + soloInstrument?.let { + it.voice.step(root, chordNotes, index) + } ?: run { + for (instrument in Instrument.instruments) { + instrument.voice.step(root, chordNotes, index) + } } return index } diff --git a/app/src/main/java/com/lukas/music/song/chords/Chord.kt b/app/src/main/java/com/lukas/music/song/chords/Chord.kt index db45fdd..4400dae 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Chord.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Chord.kt @@ -19,6 +19,12 @@ interval = Interval(value) } var interval = Interval(note) + set(value) { + field = value + if (note != value.distance) { + note = value.distance + } + } fun getNotes(root: Note): Array { return Array(chordType.notes.size) { root + note + chordType.notes[it] } diff --git a/app/src/main/java/com/lukas/music/song/chords/Interval.kt b/app/src/main/java/com/lukas/music/song/chords/Interval.kt index 2726787..d112805 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Interval.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Interval.kt @@ -10,7 +10,7 @@ package com.lukas.music.song.chords -class Interval(private val distance: Int) { +class Interval(val distance: Int) { val name: IntervalName = when (distance) { 0 -> IntervalName.UNISON 1, 2 -> IntervalName.SECOND @@ -32,7 +32,7 @@ return name.toString() } - enum class IntervalName(val distance: Int, val romanVersion: String) { + enum class IntervalName(private val distance: Int, val romanVersion: String) { UNISON(0, "I"), SECOND(1, "II"), THIRD(3, "III"), diff --git a/.idea/misc.xml b/.idea/misc.xml index 97f8bf5..3d886c4 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,6 +11,8 @@ + + diff --git a/app/build.gradle b/app/build.gradle index 35173c4..97ec2a0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -56,12 +56,12 @@ } dependencies { - implementation 'androidx.core:core-ktx:1.7.0' implementation 'androidx.appcompat:appcompat:1.3.0' implementation 'com.google.android.material:material:1.6.1' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'androidx.gridlayout:gridlayout:1.0.0' + implementation 'org.jetbrains.kotlin:kotlin-reflect:1.7.20-Beta' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index dc80bd1..8e98b44 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -15,6 +15,9 @@ } AudioHost *thiz = static_cast(userData); for (auto const &instrument: *thiz->instruments) { + if (!instrument) { + continue; + } instrument->render(buffer, sampleCount); } for (uint32_t i = 0; i < sampleCount; i++) { diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 6ebd2b0..5505a35 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -8,7 +8,7 @@ static AudioHost *audioHost; -template +template void *listGet(_InputIterator iterator, uint32_t n) { for (uint32_t i = 0; i < n; i++) { iterator++; @@ -16,6 +16,18 @@ return *iterator; } +Instrument *getInstrument(uint32_t id) { + return static_cast(listGet(audioHost->instruments->begin(), id)); +} + +template +void listSet(_InputIterator iterator, uint32_t n, void *value) { + for (uint32_t i = 0; i < n; i++) { + iterator++; + } + *iterator = static_cast(value); +} + extern "C" { JNIEXPORT void JNICALL @@ -23,7 +35,6 @@ audioHost = new AudioHost(); } -extern "C" JNIEXPORT jint JNICALL Java_com_lukas_music_instruments_InternalInstrument_createInstrument(JNIEnv *env, jobject thiz) { uint32_t result = audioHost->instruments->size(); @@ -32,43 +43,37 @@ return result; } -extern "C" -JNIEXPORT void JNICALL -Java_com_lukas_music_instruments_InternalInstrument_setInstrumentActive(JNIEnv *env, jobject thiz, - jint id, jboolean active) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->wave->amplitude = active ? 0.3 : 0.0; -} -} -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_startNote(JNIEnv *env, jobject thiz, jint id, jdouble frequency) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->startNote(frequency); + getInstrument(id)->startNote(frequency); } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_endNote(JNIEnv *env, jobject thiz, jint id) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->endNote(); + getInstrument(id)->endNote(); } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_ui_fragments_PlayFragment_setMasterVolume(JNIEnv *env, jobject thiz, jdouble volume) { audioHost->masterVolume = volume; } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setInstrumentWaveform(JNIEnv *env, jobject thiz, jint id, jint waveform) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->setWaveform(static_cast(waveform)); + getInstrument(id)->setWaveform(static_cast(waveform)); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, + jfloat volume) { + getInstrument(id)->wave->amplitude = volume; +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_destroy(JNIEnv *env, jobject thiz, jint id) { + listSet(audioHost->instruments->begin(), id, nullptr); + delete getInstrument(id); +} } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt new file mode 100644 index 0000000..56f21e6 --- /dev/null +++ b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2022 Lukas Eisenhauer + * + * This program 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, either version 3 of the License, or(at your option) any later version. + * + * This program 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 this program. If not, see . + */ + +package com.lukas.music + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TableRow +import androidx.core.view.setMargins +import androidx.fragment.app.DialogFragment +import com.google.android.material.button.MaterialButton +import com.lukas.music.databinding.FragmentEditVoiceBinding +import com.lukas.music.song.Song +import com.lukas.music.song.voice.Voice +import com.lukas.music.util.ArrayProperty +import com.lukas.music.util.setupToggle + +class EditVoiceFragment(private val voice: Voice) : DialogFragment() { + private lateinit var binding: FragmentEditVoiceBinding + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEditVoiceBinding.inflate(inflater) + for (row in 0 until voice.noteCount) { + val rowLayout = TableRow(binding.root.context) + for (column in 0 until Song.currentSong.beats) { + val button = MaterialButton(binding.root.context) + button.layoutParams = buttonLayout + button.setupToggle(ArrayProperty(voice.noteActive[column], row), R.color.blue) + rowLayout.addView(button) + } + binding.noteGrid.addView(rowLayout) + } + binding.noteGrid.isStretchAllColumns = true + binding.closeButton.setOnClickListener { + dismiss() + } + return binding.root + } + + override fun onStart() { + super.onStart() + dialog?.window?.setLayout( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + } + + companion object { + val buttonLayout = TableRow.LayoutParams(0, TableRow.LayoutParams.WRAP_CONTENT) + + init { + buttonLayout.setMargins(5) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Instrument.kt b/app/src/main/java/com/lukas/music/instruments/Instrument.kt index 8ad90c6..92a896c 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -10,42 +10,22 @@ package com.lukas.music.instruments -import com.lukas.music.databinding.FragmentInstrumentBinding import com.lukas.music.song.note.Note import com.lukas.music.song.voice.BassVoice -import com.lukas.music.song.voice.ChordVoice import com.lukas.music.song.voice.Voice -abstract class Instrument(private var name: String) { - private var active = false +abstract class Instrument(var name: String) { + var voice: Voice = BassVoice(this) abstract var waveform: Waveform - - fun applyToView(binding: FragmentInstrumentBinding) { - binding.instrumentNameText.text = name - binding.editInstrumentButton.setOnClickListener { - println("click instrument $name") - } - binding.activeSwitch.setOnCheckedChangeListener { _, newActive -> - active = newActive - changeActive(newActive) - } - binding.activeSwitch.isChecked = active - } + abstract var volume: Float + abstract var muted: Boolean abstract fun startNote(note: Note) abstract fun stop() - abstract fun changeActive(newActive: Boolean) + abstract fun stopNote(note: Note) + abstract fun destroy() companion object { - val instruments = - mutableListOf( - MonoInstrument("Bass"), - PolyInstrument("Chords"), - ) - - val voice = mutableListOf( - BassVoice(instruments[0]), - ChordVoice(instruments[1]), - ) + val instruments = mutableListOf() } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt index dc64bcc..b4c68a4 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -10,33 +10,67 @@ package com.lukas.music.instruments +import com.lukas.music.song.note.Note + class InternalInstrument { private val id = createInstrument() - var active: Boolean = false - set(value) { - field = value - setInstrumentActive(id, value) - } + var note: Note? = null var waveform: Waveform = Waveform.SINE set(value) { field = value setInstrumentWaveform(id, value.id) - // this is to resend the setInstrumentActive for the new waveform in the internal c++ code - active = active + refresh() } - fun startNote(frequency: Double) { - startNote(id, frequency) + var volume: Float = 0.3f + set(value) { + field = value + if (!muted) { + actualVolume = value + } + } + + var muted: Boolean = false + set(value) { + field = value + actualVolume = if (value) 0.0f else volume + } + + private var actualVolume: Float = 1.0f + set(value) { + field = value + setVolume(id, value) + } + + init { + refresh() + } + + private fun refresh() { + // this is to resend the old information to the internal c++ code (when changing the waveform) + muted = muted + volume = volume + } + + fun startNote(note: Note) { + this.note = note + startNote(id, note.frequency) } fun endNote() { + note = null endNote(id) } + fun destroy() { + destroy(id) + } + private external fun createInstrument(): Int - private external fun setInstrumentActive(id: Int, isActive: Boolean) private external fun setInstrumentWaveform(id: Int, waveform: Int) private external fun startNote(id: Int, frequency: Double) private external fun endNote(id: Int) + private external fun setVolume(id: Int, volume: Float) + private external fun destroy(id: Int) } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt index e7b7464..2b43524 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -21,15 +21,36 @@ internalInstrument.waveform = value } - override fun startNote(note: Note) { - internalInstrument.startNote(note.frequency) - } + override var volume: Float = 1.0f + set(value) { + field = value + internalInstrument.volume = volume + } - override fun changeActive(newActive: Boolean) { - internalInstrument.active = newActive + override var muted: Boolean = false + set(value) { + field = value + internalInstrument.muted = value + } + + override fun startNote(note: Note) { + if (note == internalInstrument.note) { + return + } + internalInstrument.startNote(note) } override fun stop() { internalInstrument.endNote() } + + override fun stopNote(note: Note) { + if (note == internalInstrument.note) { + stop() + } + } + + override fun destroy() { + internalInstrument.destroy() + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt index 9d8b4f9..26e4fbb 100644 --- a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt @@ -24,27 +24,55 @@ } } + override var volume: Float = 1.0f + set(value) { + field = value + for (internalInstrument in internalInstruments) { + internalInstrument.volume = volume + } + } + + override var muted: Boolean = false + set(value) { + field = value + for (instrument in internalInstruments) { + instrument.muted = value + } + } + override fun startNote(note: Note) { for ((index, instrumentPlaying) in playing.withIndex()) { if (!instrumentPlaying) { - internalInstruments[index].startNote(note.frequency) + internalInstruments[index].startNote(note) playing[index] = true return } + if (internalInstruments[index].note == note) { + return + } } throw IllegalStateException("cannot start another note with the current amount of oscillators") } - override fun changeActive(newActive: Boolean) { - for (instrument in internalInstruments) { - instrument.active = newActive - } - } - override fun stop() { for ((i, instrument) in internalInstruments.withIndex()) { instrument.endNote() playing[i] = false } } + + override fun stopNote(note: Note) { + for ((i, instrument) in internalInstruments.withIndex()) { + if (instrument.note == note) { + instrument.endNote() + playing[i] = false + } + } + } + + override fun destroy() { + for (instrument in internalInstruments) { + instrument.destroy() + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/Song.kt b/app/src/main/java/com/lukas/music/song/Song.kt index a3123f2..fc5421b 100644 --- a/app/src/main/java/com/lukas/music/song/Song.kt +++ b/app/src/main/java/com/lukas/music/song/Song.kt @@ -16,10 +16,33 @@ import com.lukas.music.util.Cycle class Song( - var root: Note, + root: Note, val beats: Int ) : Cycle(beats) { val chordProgression = ChordProgression() + var soloInstrument: Instrument? = null + set(value) { + field = value + value?.let { + for (instrument in Instrument.instruments) { + if (instrument != value) { + instrument.stop() + } + } + } + } + + var root: Note = root + set(value) { + field = value + stopAllInstruments() + } + + private fun stopAllInstruments() { + for (instrument in Instrument.instruments) { + instrument.stop() + } + } init { for (i in 0 until beats) { @@ -27,6 +50,7 @@ } wraparoundListeners += { chordProgression.step() + stopAllInstruments() } } @@ -34,8 +58,12 @@ super.step() val chord = chordProgression.currentItem?.currentItem ?: return index val chordNotes = chord.getNotes(root) - for (voice in Instrument.voice) { - voice.step(root, chordNotes) + soloInstrument?.let { + it.voice.step(root, chordNotes, index) + } ?: run { + for (instrument in Instrument.instruments) { + instrument.voice.step(root, chordNotes, index) + } } return index } diff --git a/app/src/main/java/com/lukas/music/song/chords/Chord.kt b/app/src/main/java/com/lukas/music/song/chords/Chord.kt index db45fdd..4400dae 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Chord.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Chord.kt @@ -19,6 +19,12 @@ interval = Interval(value) } var interval = Interval(note) + set(value) { + field = value + if (note != value.distance) { + note = value.distance + } + } fun getNotes(root: Note): Array { return Array(chordType.notes.size) { root + note + chordType.notes[it] } diff --git a/app/src/main/java/com/lukas/music/song/chords/Interval.kt b/app/src/main/java/com/lukas/music/song/chords/Interval.kt index 2726787..d112805 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Interval.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Interval.kt @@ -10,7 +10,7 @@ package com.lukas.music.song.chords -class Interval(private val distance: Int) { +class Interval(val distance: Int) { val name: IntervalName = when (distance) { 0 -> IntervalName.UNISON 1, 2 -> IntervalName.SECOND @@ -32,7 +32,7 @@ return name.toString() } - enum class IntervalName(val distance: Int, val romanVersion: String) { + enum class IntervalName(private val distance: Int, val romanVersion: String) { UNISON(0, "I"), SECOND(1, "II"), THIRD(3, "III"), diff --git a/app/src/main/java/com/lukas/music/song/note/Note.kt b/app/src/main/java/com/lukas/music/song/note/Note.kt index d3a588d..17bfb2b 100644 --- a/app/src/main/java/com/lukas/music/song/note/Note.kt +++ b/app/src/main/java/com/lukas/music/song/note/Note.kt @@ -28,6 +28,22 @@ return this + (-other) } + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Note + + if (id != other.id) return false + + return true + } + + override fun hashCode(): Int { + return id + } + + companion object { val NOTES = Array(128) { Note(it) } diff --git a/.idea/misc.xml b/.idea/misc.xml index 97f8bf5..3d886c4 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,6 +11,8 @@ + + diff --git a/app/build.gradle b/app/build.gradle index 35173c4..97ec2a0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -56,12 +56,12 @@ } dependencies { - implementation 'androidx.core:core-ktx:1.7.0' implementation 'androidx.appcompat:appcompat:1.3.0' implementation 'com.google.android.material:material:1.6.1' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'androidx.gridlayout:gridlayout:1.0.0' + implementation 'org.jetbrains.kotlin:kotlin-reflect:1.7.20-Beta' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index dc80bd1..8e98b44 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -15,6 +15,9 @@ } AudioHost *thiz = static_cast(userData); for (auto const &instrument: *thiz->instruments) { + if (!instrument) { + continue; + } instrument->render(buffer, sampleCount); } for (uint32_t i = 0; i < sampleCount; i++) { diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 6ebd2b0..5505a35 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -8,7 +8,7 @@ static AudioHost *audioHost; -template +template void *listGet(_InputIterator iterator, uint32_t n) { for (uint32_t i = 0; i < n; i++) { iterator++; @@ -16,6 +16,18 @@ return *iterator; } +Instrument *getInstrument(uint32_t id) { + return static_cast(listGet(audioHost->instruments->begin(), id)); +} + +template +void listSet(_InputIterator iterator, uint32_t n, void *value) { + for (uint32_t i = 0; i < n; i++) { + iterator++; + } + *iterator = static_cast(value); +} + extern "C" { JNIEXPORT void JNICALL @@ -23,7 +35,6 @@ audioHost = new AudioHost(); } -extern "C" JNIEXPORT jint JNICALL Java_com_lukas_music_instruments_InternalInstrument_createInstrument(JNIEnv *env, jobject thiz) { uint32_t result = audioHost->instruments->size(); @@ -32,43 +43,37 @@ return result; } -extern "C" -JNIEXPORT void JNICALL -Java_com_lukas_music_instruments_InternalInstrument_setInstrumentActive(JNIEnv *env, jobject thiz, - jint id, jboolean active) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->wave->amplitude = active ? 0.3 : 0.0; -} -} -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_startNote(JNIEnv *env, jobject thiz, jint id, jdouble frequency) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->startNote(frequency); + getInstrument(id)->startNote(frequency); } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_endNote(JNIEnv *env, jobject thiz, jint id) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->endNote(); + getInstrument(id)->endNote(); } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_ui_fragments_PlayFragment_setMasterVolume(JNIEnv *env, jobject thiz, jdouble volume) { audioHost->masterVolume = volume; } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setInstrumentWaveform(JNIEnv *env, jobject thiz, jint id, jint waveform) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->setWaveform(static_cast(waveform)); + getInstrument(id)->setWaveform(static_cast(waveform)); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, + jfloat volume) { + getInstrument(id)->wave->amplitude = volume; +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_destroy(JNIEnv *env, jobject thiz, jint id) { + listSet(audioHost->instruments->begin(), id, nullptr); + delete getInstrument(id); +} } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt new file mode 100644 index 0000000..56f21e6 --- /dev/null +++ b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2022 Lukas Eisenhauer + * + * This program 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, either version 3 of the License, or(at your option) any later version. + * + * This program 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 this program. If not, see . + */ + +package com.lukas.music + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TableRow +import androidx.core.view.setMargins +import androidx.fragment.app.DialogFragment +import com.google.android.material.button.MaterialButton +import com.lukas.music.databinding.FragmentEditVoiceBinding +import com.lukas.music.song.Song +import com.lukas.music.song.voice.Voice +import com.lukas.music.util.ArrayProperty +import com.lukas.music.util.setupToggle + +class EditVoiceFragment(private val voice: Voice) : DialogFragment() { + private lateinit var binding: FragmentEditVoiceBinding + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEditVoiceBinding.inflate(inflater) + for (row in 0 until voice.noteCount) { + val rowLayout = TableRow(binding.root.context) + for (column in 0 until Song.currentSong.beats) { + val button = MaterialButton(binding.root.context) + button.layoutParams = buttonLayout + button.setupToggle(ArrayProperty(voice.noteActive[column], row), R.color.blue) + rowLayout.addView(button) + } + binding.noteGrid.addView(rowLayout) + } + binding.noteGrid.isStretchAllColumns = true + binding.closeButton.setOnClickListener { + dismiss() + } + return binding.root + } + + override fun onStart() { + super.onStart() + dialog?.window?.setLayout( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + } + + companion object { + val buttonLayout = TableRow.LayoutParams(0, TableRow.LayoutParams.WRAP_CONTENT) + + init { + buttonLayout.setMargins(5) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Instrument.kt b/app/src/main/java/com/lukas/music/instruments/Instrument.kt index 8ad90c6..92a896c 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -10,42 +10,22 @@ package com.lukas.music.instruments -import com.lukas.music.databinding.FragmentInstrumentBinding import com.lukas.music.song.note.Note import com.lukas.music.song.voice.BassVoice -import com.lukas.music.song.voice.ChordVoice import com.lukas.music.song.voice.Voice -abstract class Instrument(private var name: String) { - private var active = false +abstract class Instrument(var name: String) { + var voice: Voice = BassVoice(this) abstract var waveform: Waveform - - fun applyToView(binding: FragmentInstrumentBinding) { - binding.instrumentNameText.text = name - binding.editInstrumentButton.setOnClickListener { - println("click instrument $name") - } - binding.activeSwitch.setOnCheckedChangeListener { _, newActive -> - active = newActive - changeActive(newActive) - } - binding.activeSwitch.isChecked = active - } + abstract var volume: Float + abstract var muted: Boolean abstract fun startNote(note: Note) abstract fun stop() - abstract fun changeActive(newActive: Boolean) + abstract fun stopNote(note: Note) + abstract fun destroy() companion object { - val instruments = - mutableListOf( - MonoInstrument("Bass"), - PolyInstrument("Chords"), - ) - - val voice = mutableListOf( - BassVoice(instruments[0]), - ChordVoice(instruments[1]), - ) + val instruments = mutableListOf() } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt index dc64bcc..b4c68a4 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -10,33 +10,67 @@ package com.lukas.music.instruments +import com.lukas.music.song.note.Note + class InternalInstrument { private val id = createInstrument() - var active: Boolean = false - set(value) { - field = value - setInstrumentActive(id, value) - } + var note: Note? = null var waveform: Waveform = Waveform.SINE set(value) { field = value setInstrumentWaveform(id, value.id) - // this is to resend the setInstrumentActive for the new waveform in the internal c++ code - active = active + refresh() } - fun startNote(frequency: Double) { - startNote(id, frequency) + var volume: Float = 0.3f + set(value) { + field = value + if (!muted) { + actualVolume = value + } + } + + var muted: Boolean = false + set(value) { + field = value + actualVolume = if (value) 0.0f else volume + } + + private var actualVolume: Float = 1.0f + set(value) { + field = value + setVolume(id, value) + } + + init { + refresh() + } + + private fun refresh() { + // this is to resend the old information to the internal c++ code (when changing the waveform) + muted = muted + volume = volume + } + + fun startNote(note: Note) { + this.note = note + startNote(id, note.frequency) } fun endNote() { + note = null endNote(id) } + fun destroy() { + destroy(id) + } + private external fun createInstrument(): Int - private external fun setInstrumentActive(id: Int, isActive: Boolean) private external fun setInstrumentWaveform(id: Int, waveform: Int) private external fun startNote(id: Int, frequency: Double) private external fun endNote(id: Int) + private external fun setVolume(id: Int, volume: Float) + private external fun destroy(id: Int) } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt index e7b7464..2b43524 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -21,15 +21,36 @@ internalInstrument.waveform = value } - override fun startNote(note: Note) { - internalInstrument.startNote(note.frequency) - } + override var volume: Float = 1.0f + set(value) { + field = value + internalInstrument.volume = volume + } - override fun changeActive(newActive: Boolean) { - internalInstrument.active = newActive + override var muted: Boolean = false + set(value) { + field = value + internalInstrument.muted = value + } + + override fun startNote(note: Note) { + if (note == internalInstrument.note) { + return + } + internalInstrument.startNote(note) } override fun stop() { internalInstrument.endNote() } + + override fun stopNote(note: Note) { + if (note == internalInstrument.note) { + stop() + } + } + + override fun destroy() { + internalInstrument.destroy() + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt index 9d8b4f9..26e4fbb 100644 --- a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt @@ -24,27 +24,55 @@ } } + override var volume: Float = 1.0f + set(value) { + field = value + for (internalInstrument in internalInstruments) { + internalInstrument.volume = volume + } + } + + override var muted: Boolean = false + set(value) { + field = value + for (instrument in internalInstruments) { + instrument.muted = value + } + } + override fun startNote(note: Note) { for ((index, instrumentPlaying) in playing.withIndex()) { if (!instrumentPlaying) { - internalInstruments[index].startNote(note.frequency) + internalInstruments[index].startNote(note) playing[index] = true return } + if (internalInstruments[index].note == note) { + return + } } throw IllegalStateException("cannot start another note with the current amount of oscillators") } - override fun changeActive(newActive: Boolean) { - for (instrument in internalInstruments) { - instrument.active = newActive - } - } - override fun stop() { for ((i, instrument) in internalInstruments.withIndex()) { instrument.endNote() playing[i] = false } } + + override fun stopNote(note: Note) { + for ((i, instrument) in internalInstruments.withIndex()) { + if (instrument.note == note) { + instrument.endNote() + playing[i] = false + } + } + } + + override fun destroy() { + for (instrument in internalInstruments) { + instrument.destroy() + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/Song.kt b/app/src/main/java/com/lukas/music/song/Song.kt index a3123f2..fc5421b 100644 --- a/app/src/main/java/com/lukas/music/song/Song.kt +++ b/app/src/main/java/com/lukas/music/song/Song.kt @@ -16,10 +16,33 @@ import com.lukas.music.util.Cycle class Song( - var root: Note, + root: Note, val beats: Int ) : Cycle(beats) { val chordProgression = ChordProgression() + var soloInstrument: Instrument? = null + set(value) { + field = value + value?.let { + for (instrument in Instrument.instruments) { + if (instrument != value) { + instrument.stop() + } + } + } + } + + var root: Note = root + set(value) { + field = value + stopAllInstruments() + } + + private fun stopAllInstruments() { + for (instrument in Instrument.instruments) { + instrument.stop() + } + } init { for (i in 0 until beats) { @@ -27,6 +50,7 @@ } wraparoundListeners += { chordProgression.step() + stopAllInstruments() } } @@ -34,8 +58,12 @@ super.step() val chord = chordProgression.currentItem?.currentItem ?: return index val chordNotes = chord.getNotes(root) - for (voice in Instrument.voice) { - voice.step(root, chordNotes) + soloInstrument?.let { + it.voice.step(root, chordNotes, index) + } ?: run { + for (instrument in Instrument.instruments) { + instrument.voice.step(root, chordNotes, index) + } } return index } diff --git a/app/src/main/java/com/lukas/music/song/chords/Chord.kt b/app/src/main/java/com/lukas/music/song/chords/Chord.kt index db45fdd..4400dae 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Chord.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Chord.kt @@ -19,6 +19,12 @@ interval = Interval(value) } var interval = Interval(note) + set(value) { + field = value + if (note != value.distance) { + note = value.distance + } + } fun getNotes(root: Note): Array { return Array(chordType.notes.size) { root + note + chordType.notes[it] } diff --git a/app/src/main/java/com/lukas/music/song/chords/Interval.kt b/app/src/main/java/com/lukas/music/song/chords/Interval.kt index 2726787..d112805 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Interval.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Interval.kt @@ -10,7 +10,7 @@ package com.lukas.music.song.chords -class Interval(private val distance: Int) { +class Interval(val distance: Int) { val name: IntervalName = when (distance) { 0 -> IntervalName.UNISON 1, 2 -> IntervalName.SECOND @@ -32,7 +32,7 @@ return name.toString() } - enum class IntervalName(val distance: Int, val romanVersion: String) { + enum class IntervalName(private val distance: Int, val romanVersion: String) { UNISON(0, "I"), SECOND(1, "II"), THIRD(3, "III"), diff --git a/app/src/main/java/com/lukas/music/song/note/Note.kt b/app/src/main/java/com/lukas/music/song/note/Note.kt index d3a588d..17bfb2b 100644 --- a/app/src/main/java/com/lukas/music/song/note/Note.kt +++ b/app/src/main/java/com/lukas/music/song/note/Note.kt @@ -28,6 +28,22 @@ return this + (-other) } + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Note + + if (id != other.id) return false + + return true + } + + override fun hashCode(): Int { + return id + } + + companion object { val NOTES = Array(128) { Note(it) } diff --git a/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt b/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt index 3990554..4706068 100644 --- a/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt +++ b/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt @@ -14,9 +14,16 @@ import com.lukas.music.song.note.Note class BassVoice(instrument: Instrument) : Voice(instrument) { - override val steps = listOf(1, 3) + override var noteActive: Array> = arrayOf( + arrayOf(true), + arrayOf(false), + arrayOf(true), + arrayOf(false) + ) - override fun step(root: Note, chord: Array) { - instrument.startNote(chord[0] - 24) + override val noteCount: Int = 1 + + override fun getNotes(root: Note, chordNotes: Array): Array { + return arrayOf(chordNotes[0] - 24) } } \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 97f8bf5..3d886c4 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,6 +11,8 @@ + + diff --git a/app/build.gradle b/app/build.gradle index 35173c4..97ec2a0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -56,12 +56,12 @@ } dependencies { - implementation 'androidx.core:core-ktx:1.7.0' implementation 'androidx.appcompat:appcompat:1.3.0' implementation 'com.google.android.material:material:1.6.1' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'androidx.gridlayout:gridlayout:1.0.0' + implementation 'org.jetbrains.kotlin:kotlin-reflect:1.7.20-Beta' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index dc80bd1..8e98b44 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -15,6 +15,9 @@ } AudioHost *thiz = static_cast(userData); for (auto const &instrument: *thiz->instruments) { + if (!instrument) { + continue; + } instrument->render(buffer, sampleCount); } for (uint32_t i = 0; i < sampleCount; i++) { diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 6ebd2b0..5505a35 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -8,7 +8,7 @@ static AudioHost *audioHost; -template +template void *listGet(_InputIterator iterator, uint32_t n) { for (uint32_t i = 0; i < n; i++) { iterator++; @@ -16,6 +16,18 @@ return *iterator; } +Instrument *getInstrument(uint32_t id) { + return static_cast(listGet(audioHost->instruments->begin(), id)); +} + +template +void listSet(_InputIterator iterator, uint32_t n, void *value) { + for (uint32_t i = 0; i < n; i++) { + iterator++; + } + *iterator = static_cast(value); +} + extern "C" { JNIEXPORT void JNICALL @@ -23,7 +35,6 @@ audioHost = new AudioHost(); } -extern "C" JNIEXPORT jint JNICALL Java_com_lukas_music_instruments_InternalInstrument_createInstrument(JNIEnv *env, jobject thiz) { uint32_t result = audioHost->instruments->size(); @@ -32,43 +43,37 @@ return result; } -extern "C" -JNIEXPORT void JNICALL -Java_com_lukas_music_instruments_InternalInstrument_setInstrumentActive(JNIEnv *env, jobject thiz, - jint id, jboolean active) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->wave->amplitude = active ? 0.3 : 0.0; -} -} -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_startNote(JNIEnv *env, jobject thiz, jint id, jdouble frequency) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->startNote(frequency); + getInstrument(id)->startNote(frequency); } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_endNote(JNIEnv *env, jobject thiz, jint id) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->endNote(); + getInstrument(id)->endNote(); } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_ui_fragments_PlayFragment_setMasterVolume(JNIEnv *env, jobject thiz, jdouble volume) { audioHost->masterVolume = volume; } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setInstrumentWaveform(JNIEnv *env, jobject thiz, jint id, jint waveform) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->setWaveform(static_cast(waveform)); + getInstrument(id)->setWaveform(static_cast(waveform)); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, + jfloat volume) { + getInstrument(id)->wave->amplitude = volume; +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_destroy(JNIEnv *env, jobject thiz, jint id) { + listSet(audioHost->instruments->begin(), id, nullptr); + delete getInstrument(id); +} } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt new file mode 100644 index 0000000..56f21e6 --- /dev/null +++ b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2022 Lukas Eisenhauer + * + * This program 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, either version 3 of the License, or(at your option) any later version. + * + * This program 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 this program. If not, see . + */ + +package com.lukas.music + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TableRow +import androidx.core.view.setMargins +import androidx.fragment.app.DialogFragment +import com.google.android.material.button.MaterialButton +import com.lukas.music.databinding.FragmentEditVoiceBinding +import com.lukas.music.song.Song +import com.lukas.music.song.voice.Voice +import com.lukas.music.util.ArrayProperty +import com.lukas.music.util.setupToggle + +class EditVoiceFragment(private val voice: Voice) : DialogFragment() { + private lateinit var binding: FragmentEditVoiceBinding + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEditVoiceBinding.inflate(inflater) + for (row in 0 until voice.noteCount) { + val rowLayout = TableRow(binding.root.context) + for (column in 0 until Song.currentSong.beats) { + val button = MaterialButton(binding.root.context) + button.layoutParams = buttonLayout + button.setupToggle(ArrayProperty(voice.noteActive[column], row), R.color.blue) + rowLayout.addView(button) + } + binding.noteGrid.addView(rowLayout) + } + binding.noteGrid.isStretchAllColumns = true + binding.closeButton.setOnClickListener { + dismiss() + } + return binding.root + } + + override fun onStart() { + super.onStart() + dialog?.window?.setLayout( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + } + + companion object { + val buttonLayout = TableRow.LayoutParams(0, TableRow.LayoutParams.WRAP_CONTENT) + + init { + buttonLayout.setMargins(5) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Instrument.kt b/app/src/main/java/com/lukas/music/instruments/Instrument.kt index 8ad90c6..92a896c 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -10,42 +10,22 @@ package com.lukas.music.instruments -import com.lukas.music.databinding.FragmentInstrumentBinding import com.lukas.music.song.note.Note import com.lukas.music.song.voice.BassVoice -import com.lukas.music.song.voice.ChordVoice import com.lukas.music.song.voice.Voice -abstract class Instrument(private var name: String) { - private var active = false +abstract class Instrument(var name: String) { + var voice: Voice = BassVoice(this) abstract var waveform: Waveform - - fun applyToView(binding: FragmentInstrumentBinding) { - binding.instrumentNameText.text = name - binding.editInstrumentButton.setOnClickListener { - println("click instrument $name") - } - binding.activeSwitch.setOnCheckedChangeListener { _, newActive -> - active = newActive - changeActive(newActive) - } - binding.activeSwitch.isChecked = active - } + abstract var volume: Float + abstract var muted: Boolean abstract fun startNote(note: Note) abstract fun stop() - abstract fun changeActive(newActive: Boolean) + abstract fun stopNote(note: Note) + abstract fun destroy() companion object { - val instruments = - mutableListOf( - MonoInstrument("Bass"), - PolyInstrument("Chords"), - ) - - val voice = mutableListOf( - BassVoice(instruments[0]), - ChordVoice(instruments[1]), - ) + val instruments = mutableListOf() } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt index dc64bcc..b4c68a4 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -10,33 +10,67 @@ package com.lukas.music.instruments +import com.lukas.music.song.note.Note + class InternalInstrument { private val id = createInstrument() - var active: Boolean = false - set(value) { - field = value - setInstrumentActive(id, value) - } + var note: Note? = null var waveform: Waveform = Waveform.SINE set(value) { field = value setInstrumentWaveform(id, value.id) - // this is to resend the setInstrumentActive for the new waveform in the internal c++ code - active = active + refresh() } - fun startNote(frequency: Double) { - startNote(id, frequency) + var volume: Float = 0.3f + set(value) { + field = value + if (!muted) { + actualVolume = value + } + } + + var muted: Boolean = false + set(value) { + field = value + actualVolume = if (value) 0.0f else volume + } + + private var actualVolume: Float = 1.0f + set(value) { + field = value + setVolume(id, value) + } + + init { + refresh() + } + + private fun refresh() { + // this is to resend the old information to the internal c++ code (when changing the waveform) + muted = muted + volume = volume + } + + fun startNote(note: Note) { + this.note = note + startNote(id, note.frequency) } fun endNote() { + note = null endNote(id) } + fun destroy() { + destroy(id) + } + private external fun createInstrument(): Int - private external fun setInstrumentActive(id: Int, isActive: Boolean) private external fun setInstrumentWaveform(id: Int, waveform: Int) private external fun startNote(id: Int, frequency: Double) private external fun endNote(id: Int) + private external fun setVolume(id: Int, volume: Float) + private external fun destroy(id: Int) } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt index e7b7464..2b43524 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -21,15 +21,36 @@ internalInstrument.waveform = value } - override fun startNote(note: Note) { - internalInstrument.startNote(note.frequency) - } + override var volume: Float = 1.0f + set(value) { + field = value + internalInstrument.volume = volume + } - override fun changeActive(newActive: Boolean) { - internalInstrument.active = newActive + override var muted: Boolean = false + set(value) { + field = value + internalInstrument.muted = value + } + + override fun startNote(note: Note) { + if (note == internalInstrument.note) { + return + } + internalInstrument.startNote(note) } override fun stop() { internalInstrument.endNote() } + + override fun stopNote(note: Note) { + if (note == internalInstrument.note) { + stop() + } + } + + override fun destroy() { + internalInstrument.destroy() + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt index 9d8b4f9..26e4fbb 100644 --- a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt @@ -24,27 +24,55 @@ } } + override var volume: Float = 1.0f + set(value) { + field = value + for (internalInstrument in internalInstruments) { + internalInstrument.volume = volume + } + } + + override var muted: Boolean = false + set(value) { + field = value + for (instrument in internalInstruments) { + instrument.muted = value + } + } + override fun startNote(note: Note) { for ((index, instrumentPlaying) in playing.withIndex()) { if (!instrumentPlaying) { - internalInstruments[index].startNote(note.frequency) + internalInstruments[index].startNote(note) playing[index] = true return } + if (internalInstruments[index].note == note) { + return + } } throw IllegalStateException("cannot start another note with the current amount of oscillators") } - override fun changeActive(newActive: Boolean) { - for (instrument in internalInstruments) { - instrument.active = newActive - } - } - override fun stop() { for ((i, instrument) in internalInstruments.withIndex()) { instrument.endNote() playing[i] = false } } + + override fun stopNote(note: Note) { + for ((i, instrument) in internalInstruments.withIndex()) { + if (instrument.note == note) { + instrument.endNote() + playing[i] = false + } + } + } + + override fun destroy() { + for (instrument in internalInstruments) { + instrument.destroy() + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/Song.kt b/app/src/main/java/com/lukas/music/song/Song.kt index a3123f2..fc5421b 100644 --- a/app/src/main/java/com/lukas/music/song/Song.kt +++ b/app/src/main/java/com/lukas/music/song/Song.kt @@ -16,10 +16,33 @@ import com.lukas.music.util.Cycle class Song( - var root: Note, + root: Note, val beats: Int ) : Cycle(beats) { val chordProgression = ChordProgression() + var soloInstrument: Instrument? = null + set(value) { + field = value + value?.let { + for (instrument in Instrument.instruments) { + if (instrument != value) { + instrument.stop() + } + } + } + } + + var root: Note = root + set(value) { + field = value + stopAllInstruments() + } + + private fun stopAllInstruments() { + for (instrument in Instrument.instruments) { + instrument.stop() + } + } init { for (i in 0 until beats) { @@ -27,6 +50,7 @@ } wraparoundListeners += { chordProgression.step() + stopAllInstruments() } } @@ -34,8 +58,12 @@ super.step() val chord = chordProgression.currentItem?.currentItem ?: return index val chordNotes = chord.getNotes(root) - for (voice in Instrument.voice) { - voice.step(root, chordNotes) + soloInstrument?.let { + it.voice.step(root, chordNotes, index) + } ?: run { + for (instrument in Instrument.instruments) { + instrument.voice.step(root, chordNotes, index) + } } return index } diff --git a/app/src/main/java/com/lukas/music/song/chords/Chord.kt b/app/src/main/java/com/lukas/music/song/chords/Chord.kt index db45fdd..4400dae 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Chord.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Chord.kt @@ -19,6 +19,12 @@ interval = Interval(value) } var interval = Interval(note) + set(value) { + field = value + if (note != value.distance) { + note = value.distance + } + } fun getNotes(root: Note): Array { return Array(chordType.notes.size) { root + note + chordType.notes[it] } diff --git a/app/src/main/java/com/lukas/music/song/chords/Interval.kt b/app/src/main/java/com/lukas/music/song/chords/Interval.kt index 2726787..d112805 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Interval.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Interval.kt @@ -10,7 +10,7 @@ package com.lukas.music.song.chords -class Interval(private val distance: Int) { +class Interval(val distance: Int) { val name: IntervalName = when (distance) { 0 -> IntervalName.UNISON 1, 2 -> IntervalName.SECOND @@ -32,7 +32,7 @@ return name.toString() } - enum class IntervalName(val distance: Int, val romanVersion: String) { + enum class IntervalName(private val distance: Int, val romanVersion: String) { UNISON(0, "I"), SECOND(1, "II"), THIRD(3, "III"), diff --git a/app/src/main/java/com/lukas/music/song/note/Note.kt b/app/src/main/java/com/lukas/music/song/note/Note.kt index d3a588d..17bfb2b 100644 --- a/app/src/main/java/com/lukas/music/song/note/Note.kt +++ b/app/src/main/java/com/lukas/music/song/note/Note.kt @@ -28,6 +28,22 @@ return this + (-other) } + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Note + + if (id != other.id) return false + + return true + } + + override fun hashCode(): Int { + return id + } + + companion object { val NOTES = Array(128) { Note(it) } diff --git a/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt b/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt index 3990554..4706068 100644 --- a/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt +++ b/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt @@ -14,9 +14,16 @@ import com.lukas.music.song.note.Note class BassVoice(instrument: Instrument) : Voice(instrument) { - override val steps = listOf(1, 3) + override var noteActive: Array> = arrayOf( + arrayOf(true), + arrayOf(false), + arrayOf(true), + arrayOf(false) + ) - override fun step(root: Note, chord: Array) { - instrument.startNote(chord[0] - 24) + override val noteCount: Int = 1 + + override fun getNotes(root: Note, chordNotes: Array): Array { + return arrayOf(chordNotes[0] - 24) } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/voice/ChordVoice.kt b/app/src/main/java/com/lukas/music/song/voice/ChordVoice.kt index fc5362d..ab7117f 100644 --- a/app/src/main/java/com/lukas/music/song/voice/ChordVoice.kt +++ b/app/src/main/java/com/lukas/music/song/voice/ChordVoice.kt @@ -14,12 +14,15 @@ import com.lukas.music.song.note.Note class ChordVoice(instrument: Instrument) : Voice(instrument) { - override val steps: List = listOf(2, 4) + override var noteActive: Array> = arrayOf( + Array(3) { false }, + Array(3) { true }, + Array(3) { false }, + Array(3) { true }, + ) + override val noteCount: Int = 3 - override fun step(root: Note, chord: Array) { - instrument.stop() - for (note in chord) { - instrument.startNote(note) - } + override fun getNotes(root: Note, chordNotes: Array): Array { + return chordNotes } } \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 97f8bf5..3d886c4 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,6 +11,8 @@ + + diff --git a/app/build.gradle b/app/build.gradle index 35173c4..97ec2a0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -56,12 +56,12 @@ } dependencies { - implementation 'androidx.core:core-ktx:1.7.0' implementation 'androidx.appcompat:appcompat:1.3.0' implementation 'com.google.android.material:material:1.6.1' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'androidx.gridlayout:gridlayout:1.0.0' + implementation 'org.jetbrains.kotlin:kotlin-reflect:1.7.20-Beta' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index dc80bd1..8e98b44 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -15,6 +15,9 @@ } AudioHost *thiz = static_cast(userData); for (auto const &instrument: *thiz->instruments) { + if (!instrument) { + continue; + } instrument->render(buffer, sampleCount); } for (uint32_t i = 0; i < sampleCount; i++) { diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 6ebd2b0..5505a35 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -8,7 +8,7 @@ static AudioHost *audioHost; -template +template void *listGet(_InputIterator iterator, uint32_t n) { for (uint32_t i = 0; i < n; i++) { iterator++; @@ -16,6 +16,18 @@ return *iterator; } +Instrument *getInstrument(uint32_t id) { + return static_cast(listGet(audioHost->instruments->begin(), id)); +} + +template +void listSet(_InputIterator iterator, uint32_t n, void *value) { + for (uint32_t i = 0; i < n; i++) { + iterator++; + } + *iterator = static_cast(value); +} + extern "C" { JNIEXPORT void JNICALL @@ -23,7 +35,6 @@ audioHost = new AudioHost(); } -extern "C" JNIEXPORT jint JNICALL Java_com_lukas_music_instruments_InternalInstrument_createInstrument(JNIEnv *env, jobject thiz) { uint32_t result = audioHost->instruments->size(); @@ -32,43 +43,37 @@ return result; } -extern "C" -JNIEXPORT void JNICALL -Java_com_lukas_music_instruments_InternalInstrument_setInstrumentActive(JNIEnv *env, jobject thiz, - jint id, jboolean active) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->wave->amplitude = active ? 0.3 : 0.0; -} -} -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_startNote(JNIEnv *env, jobject thiz, jint id, jdouble frequency) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->startNote(frequency); + getInstrument(id)->startNote(frequency); } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_endNote(JNIEnv *env, jobject thiz, jint id) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->endNote(); + getInstrument(id)->endNote(); } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_ui_fragments_PlayFragment_setMasterVolume(JNIEnv *env, jobject thiz, jdouble volume) { audioHost->masterVolume = volume; } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setInstrumentWaveform(JNIEnv *env, jobject thiz, jint id, jint waveform) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->setWaveform(static_cast(waveform)); + getInstrument(id)->setWaveform(static_cast(waveform)); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, + jfloat volume) { + getInstrument(id)->wave->amplitude = volume; +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_destroy(JNIEnv *env, jobject thiz, jint id) { + listSet(audioHost->instruments->begin(), id, nullptr); + delete getInstrument(id); +} } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt new file mode 100644 index 0000000..56f21e6 --- /dev/null +++ b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2022 Lukas Eisenhauer + * + * This program 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, either version 3 of the License, or(at your option) any later version. + * + * This program 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 this program. If not, see . + */ + +package com.lukas.music + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TableRow +import androidx.core.view.setMargins +import androidx.fragment.app.DialogFragment +import com.google.android.material.button.MaterialButton +import com.lukas.music.databinding.FragmentEditVoiceBinding +import com.lukas.music.song.Song +import com.lukas.music.song.voice.Voice +import com.lukas.music.util.ArrayProperty +import com.lukas.music.util.setupToggle + +class EditVoiceFragment(private val voice: Voice) : DialogFragment() { + private lateinit var binding: FragmentEditVoiceBinding + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEditVoiceBinding.inflate(inflater) + for (row in 0 until voice.noteCount) { + val rowLayout = TableRow(binding.root.context) + for (column in 0 until Song.currentSong.beats) { + val button = MaterialButton(binding.root.context) + button.layoutParams = buttonLayout + button.setupToggle(ArrayProperty(voice.noteActive[column], row), R.color.blue) + rowLayout.addView(button) + } + binding.noteGrid.addView(rowLayout) + } + binding.noteGrid.isStretchAllColumns = true + binding.closeButton.setOnClickListener { + dismiss() + } + return binding.root + } + + override fun onStart() { + super.onStart() + dialog?.window?.setLayout( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + } + + companion object { + val buttonLayout = TableRow.LayoutParams(0, TableRow.LayoutParams.WRAP_CONTENT) + + init { + buttonLayout.setMargins(5) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Instrument.kt b/app/src/main/java/com/lukas/music/instruments/Instrument.kt index 8ad90c6..92a896c 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -10,42 +10,22 @@ package com.lukas.music.instruments -import com.lukas.music.databinding.FragmentInstrumentBinding import com.lukas.music.song.note.Note import com.lukas.music.song.voice.BassVoice -import com.lukas.music.song.voice.ChordVoice import com.lukas.music.song.voice.Voice -abstract class Instrument(private var name: String) { - private var active = false +abstract class Instrument(var name: String) { + var voice: Voice = BassVoice(this) abstract var waveform: Waveform - - fun applyToView(binding: FragmentInstrumentBinding) { - binding.instrumentNameText.text = name - binding.editInstrumentButton.setOnClickListener { - println("click instrument $name") - } - binding.activeSwitch.setOnCheckedChangeListener { _, newActive -> - active = newActive - changeActive(newActive) - } - binding.activeSwitch.isChecked = active - } + abstract var volume: Float + abstract var muted: Boolean abstract fun startNote(note: Note) abstract fun stop() - abstract fun changeActive(newActive: Boolean) + abstract fun stopNote(note: Note) + abstract fun destroy() companion object { - val instruments = - mutableListOf( - MonoInstrument("Bass"), - PolyInstrument("Chords"), - ) - - val voice = mutableListOf( - BassVoice(instruments[0]), - ChordVoice(instruments[1]), - ) + val instruments = mutableListOf() } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt index dc64bcc..b4c68a4 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -10,33 +10,67 @@ package com.lukas.music.instruments +import com.lukas.music.song.note.Note + class InternalInstrument { private val id = createInstrument() - var active: Boolean = false - set(value) { - field = value - setInstrumentActive(id, value) - } + var note: Note? = null var waveform: Waveform = Waveform.SINE set(value) { field = value setInstrumentWaveform(id, value.id) - // this is to resend the setInstrumentActive for the new waveform in the internal c++ code - active = active + refresh() } - fun startNote(frequency: Double) { - startNote(id, frequency) + var volume: Float = 0.3f + set(value) { + field = value + if (!muted) { + actualVolume = value + } + } + + var muted: Boolean = false + set(value) { + field = value + actualVolume = if (value) 0.0f else volume + } + + private var actualVolume: Float = 1.0f + set(value) { + field = value + setVolume(id, value) + } + + init { + refresh() + } + + private fun refresh() { + // this is to resend the old information to the internal c++ code (when changing the waveform) + muted = muted + volume = volume + } + + fun startNote(note: Note) { + this.note = note + startNote(id, note.frequency) } fun endNote() { + note = null endNote(id) } + fun destroy() { + destroy(id) + } + private external fun createInstrument(): Int - private external fun setInstrumentActive(id: Int, isActive: Boolean) private external fun setInstrumentWaveform(id: Int, waveform: Int) private external fun startNote(id: Int, frequency: Double) private external fun endNote(id: Int) + private external fun setVolume(id: Int, volume: Float) + private external fun destroy(id: Int) } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt index e7b7464..2b43524 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -21,15 +21,36 @@ internalInstrument.waveform = value } - override fun startNote(note: Note) { - internalInstrument.startNote(note.frequency) - } + override var volume: Float = 1.0f + set(value) { + field = value + internalInstrument.volume = volume + } - override fun changeActive(newActive: Boolean) { - internalInstrument.active = newActive + override var muted: Boolean = false + set(value) { + field = value + internalInstrument.muted = value + } + + override fun startNote(note: Note) { + if (note == internalInstrument.note) { + return + } + internalInstrument.startNote(note) } override fun stop() { internalInstrument.endNote() } + + override fun stopNote(note: Note) { + if (note == internalInstrument.note) { + stop() + } + } + + override fun destroy() { + internalInstrument.destroy() + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt index 9d8b4f9..26e4fbb 100644 --- a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt @@ -24,27 +24,55 @@ } } + override var volume: Float = 1.0f + set(value) { + field = value + for (internalInstrument in internalInstruments) { + internalInstrument.volume = volume + } + } + + override var muted: Boolean = false + set(value) { + field = value + for (instrument in internalInstruments) { + instrument.muted = value + } + } + override fun startNote(note: Note) { for ((index, instrumentPlaying) in playing.withIndex()) { if (!instrumentPlaying) { - internalInstruments[index].startNote(note.frequency) + internalInstruments[index].startNote(note) playing[index] = true return } + if (internalInstruments[index].note == note) { + return + } } throw IllegalStateException("cannot start another note with the current amount of oscillators") } - override fun changeActive(newActive: Boolean) { - for (instrument in internalInstruments) { - instrument.active = newActive - } - } - override fun stop() { for ((i, instrument) in internalInstruments.withIndex()) { instrument.endNote() playing[i] = false } } + + override fun stopNote(note: Note) { + for ((i, instrument) in internalInstruments.withIndex()) { + if (instrument.note == note) { + instrument.endNote() + playing[i] = false + } + } + } + + override fun destroy() { + for (instrument in internalInstruments) { + instrument.destroy() + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/Song.kt b/app/src/main/java/com/lukas/music/song/Song.kt index a3123f2..fc5421b 100644 --- a/app/src/main/java/com/lukas/music/song/Song.kt +++ b/app/src/main/java/com/lukas/music/song/Song.kt @@ -16,10 +16,33 @@ import com.lukas.music.util.Cycle class Song( - var root: Note, + root: Note, val beats: Int ) : Cycle(beats) { val chordProgression = ChordProgression() + var soloInstrument: Instrument? = null + set(value) { + field = value + value?.let { + for (instrument in Instrument.instruments) { + if (instrument != value) { + instrument.stop() + } + } + } + } + + var root: Note = root + set(value) { + field = value + stopAllInstruments() + } + + private fun stopAllInstruments() { + for (instrument in Instrument.instruments) { + instrument.stop() + } + } init { for (i in 0 until beats) { @@ -27,6 +50,7 @@ } wraparoundListeners += { chordProgression.step() + stopAllInstruments() } } @@ -34,8 +58,12 @@ super.step() val chord = chordProgression.currentItem?.currentItem ?: return index val chordNotes = chord.getNotes(root) - for (voice in Instrument.voice) { - voice.step(root, chordNotes) + soloInstrument?.let { + it.voice.step(root, chordNotes, index) + } ?: run { + for (instrument in Instrument.instruments) { + instrument.voice.step(root, chordNotes, index) + } } return index } diff --git a/app/src/main/java/com/lukas/music/song/chords/Chord.kt b/app/src/main/java/com/lukas/music/song/chords/Chord.kt index db45fdd..4400dae 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Chord.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Chord.kt @@ -19,6 +19,12 @@ interval = Interval(value) } var interval = Interval(note) + set(value) { + field = value + if (note != value.distance) { + note = value.distance + } + } fun getNotes(root: Note): Array { return Array(chordType.notes.size) { root + note + chordType.notes[it] } diff --git a/app/src/main/java/com/lukas/music/song/chords/Interval.kt b/app/src/main/java/com/lukas/music/song/chords/Interval.kt index 2726787..d112805 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Interval.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Interval.kt @@ -10,7 +10,7 @@ package com.lukas.music.song.chords -class Interval(private val distance: Int) { +class Interval(val distance: Int) { val name: IntervalName = when (distance) { 0 -> IntervalName.UNISON 1, 2 -> IntervalName.SECOND @@ -32,7 +32,7 @@ return name.toString() } - enum class IntervalName(val distance: Int, val romanVersion: String) { + enum class IntervalName(private val distance: Int, val romanVersion: String) { UNISON(0, "I"), SECOND(1, "II"), THIRD(3, "III"), diff --git a/app/src/main/java/com/lukas/music/song/note/Note.kt b/app/src/main/java/com/lukas/music/song/note/Note.kt index d3a588d..17bfb2b 100644 --- a/app/src/main/java/com/lukas/music/song/note/Note.kt +++ b/app/src/main/java/com/lukas/music/song/note/Note.kt @@ -28,6 +28,22 @@ return this + (-other) } + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Note + + if (id != other.id) return false + + return true + } + + override fun hashCode(): Int { + return id + } + + companion object { val NOTES = Array(128) { Note(it) } diff --git a/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt b/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt index 3990554..4706068 100644 --- a/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt +++ b/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt @@ -14,9 +14,16 @@ import com.lukas.music.song.note.Note class BassVoice(instrument: Instrument) : Voice(instrument) { - override val steps = listOf(1, 3) + override var noteActive: Array> = arrayOf( + arrayOf(true), + arrayOf(false), + arrayOf(true), + arrayOf(false) + ) - override fun step(root: Note, chord: Array) { - instrument.startNote(chord[0] - 24) + override val noteCount: Int = 1 + + override fun getNotes(root: Note, chordNotes: Array): Array { + return arrayOf(chordNotes[0] - 24) } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/voice/ChordVoice.kt b/app/src/main/java/com/lukas/music/song/voice/ChordVoice.kt index fc5362d..ab7117f 100644 --- a/app/src/main/java/com/lukas/music/song/voice/ChordVoice.kt +++ b/app/src/main/java/com/lukas/music/song/voice/ChordVoice.kt @@ -14,12 +14,15 @@ import com.lukas.music.song.note.Note class ChordVoice(instrument: Instrument) : Voice(instrument) { - override val steps: List = listOf(2, 4) + override var noteActive: Array> = arrayOf( + Array(3) { false }, + Array(3) { true }, + Array(3) { false }, + Array(3) { true }, + ) + override val noteCount: Int = 3 - override fun step(root: Note, chord: Array) { - instrument.stop() - for (note in chord) { - instrument.startNote(note) - } + override fun getNotes(root: Note, chordNotes: Array): Array { + return chordNotes } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/voice/Voice.kt b/app/src/main/java/com/lukas/music/song/voice/Voice.kt index f7fca94..4f56c2a 100644 --- a/app/src/main/java/com/lukas/music/song/voice/Voice.kt +++ b/app/src/main/java/com/lukas/music/song/voice/Voice.kt @@ -12,9 +12,36 @@ import com.lukas.music.instruments.Instrument import com.lukas.music.song.note.Note +import kotlin.reflect.KClass abstract class Voice(val instrument: Instrument) { - abstract val steps: List + abstract var noteActive: Array> + abstract val noteCount: Int - abstract fun step(root: Note, chord: Array) + abstract fun getNotes(root: Note, chordNotes: Array): Array + + fun step(root: Note, chordNotes: Array, beat: Int) { + if (instrument.muted) { + return + } + val activeNotes = noteActive[beat] + val notes = getNotes(root, chordNotes) + for ((index, active) in activeNotes.withIndex()) { + val note = notes[index] + if (!active) { + instrument.stopNote(note) + continue + } + instrument.startNote(note) + } + } + + companion object { + val DEFAULT_VOICES = listOf>( + BassVoice::class, + ChordVoice::class, + ) + + val DEFAULT_VOICE_NAMES = listOf("Bass", "Chord") + } } \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 97f8bf5..3d886c4 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,6 +11,8 @@ + + diff --git a/app/build.gradle b/app/build.gradle index 35173c4..97ec2a0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -56,12 +56,12 @@ } dependencies { - implementation 'androidx.core:core-ktx:1.7.0' implementation 'androidx.appcompat:appcompat:1.3.0' implementation 'com.google.android.material:material:1.6.1' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'androidx.gridlayout:gridlayout:1.0.0' + implementation 'org.jetbrains.kotlin:kotlin-reflect:1.7.20-Beta' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index dc80bd1..8e98b44 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -15,6 +15,9 @@ } AudioHost *thiz = static_cast(userData); for (auto const &instrument: *thiz->instruments) { + if (!instrument) { + continue; + } instrument->render(buffer, sampleCount); } for (uint32_t i = 0; i < sampleCount; i++) { diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 6ebd2b0..5505a35 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -8,7 +8,7 @@ static AudioHost *audioHost; -template +template void *listGet(_InputIterator iterator, uint32_t n) { for (uint32_t i = 0; i < n; i++) { iterator++; @@ -16,6 +16,18 @@ return *iterator; } +Instrument *getInstrument(uint32_t id) { + return static_cast(listGet(audioHost->instruments->begin(), id)); +} + +template +void listSet(_InputIterator iterator, uint32_t n, void *value) { + for (uint32_t i = 0; i < n; i++) { + iterator++; + } + *iterator = static_cast(value); +} + extern "C" { JNIEXPORT void JNICALL @@ -23,7 +35,6 @@ audioHost = new AudioHost(); } -extern "C" JNIEXPORT jint JNICALL Java_com_lukas_music_instruments_InternalInstrument_createInstrument(JNIEnv *env, jobject thiz) { uint32_t result = audioHost->instruments->size(); @@ -32,43 +43,37 @@ return result; } -extern "C" -JNIEXPORT void JNICALL -Java_com_lukas_music_instruments_InternalInstrument_setInstrumentActive(JNIEnv *env, jobject thiz, - jint id, jboolean active) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->wave->amplitude = active ? 0.3 : 0.0; -} -} -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_startNote(JNIEnv *env, jobject thiz, jint id, jdouble frequency) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->startNote(frequency); + getInstrument(id)->startNote(frequency); } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_endNote(JNIEnv *env, jobject thiz, jint id) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->endNote(); + getInstrument(id)->endNote(); } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_ui_fragments_PlayFragment_setMasterVolume(JNIEnv *env, jobject thiz, jdouble volume) { audioHost->masterVolume = volume; } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setInstrumentWaveform(JNIEnv *env, jobject thiz, jint id, jint waveform) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->setWaveform(static_cast(waveform)); + getInstrument(id)->setWaveform(static_cast(waveform)); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, + jfloat volume) { + getInstrument(id)->wave->amplitude = volume; +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_destroy(JNIEnv *env, jobject thiz, jint id) { + listSet(audioHost->instruments->begin(), id, nullptr); + delete getInstrument(id); +} } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt new file mode 100644 index 0000000..56f21e6 --- /dev/null +++ b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2022 Lukas Eisenhauer + * + * This program 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, either version 3 of the License, or(at your option) any later version. + * + * This program 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 this program. If not, see . + */ + +package com.lukas.music + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TableRow +import androidx.core.view.setMargins +import androidx.fragment.app.DialogFragment +import com.google.android.material.button.MaterialButton +import com.lukas.music.databinding.FragmentEditVoiceBinding +import com.lukas.music.song.Song +import com.lukas.music.song.voice.Voice +import com.lukas.music.util.ArrayProperty +import com.lukas.music.util.setupToggle + +class EditVoiceFragment(private val voice: Voice) : DialogFragment() { + private lateinit var binding: FragmentEditVoiceBinding + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEditVoiceBinding.inflate(inflater) + for (row in 0 until voice.noteCount) { + val rowLayout = TableRow(binding.root.context) + for (column in 0 until Song.currentSong.beats) { + val button = MaterialButton(binding.root.context) + button.layoutParams = buttonLayout + button.setupToggle(ArrayProperty(voice.noteActive[column], row), R.color.blue) + rowLayout.addView(button) + } + binding.noteGrid.addView(rowLayout) + } + binding.noteGrid.isStretchAllColumns = true + binding.closeButton.setOnClickListener { + dismiss() + } + return binding.root + } + + override fun onStart() { + super.onStart() + dialog?.window?.setLayout( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + } + + companion object { + val buttonLayout = TableRow.LayoutParams(0, TableRow.LayoutParams.WRAP_CONTENT) + + init { + buttonLayout.setMargins(5) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Instrument.kt b/app/src/main/java/com/lukas/music/instruments/Instrument.kt index 8ad90c6..92a896c 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -10,42 +10,22 @@ package com.lukas.music.instruments -import com.lukas.music.databinding.FragmentInstrumentBinding import com.lukas.music.song.note.Note import com.lukas.music.song.voice.BassVoice -import com.lukas.music.song.voice.ChordVoice import com.lukas.music.song.voice.Voice -abstract class Instrument(private var name: String) { - private var active = false +abstract class Instrument(var name: String) { + var voice: Voice = BassVoice(this) abstract var waveform: Waveform - - fun applyToView(binding: FragmentInstrumentBinding) { - binding.instrumentNameText.text = name - binding.editInstrumentButton.setOnClickListener { - println("click instrument $name") - } - binding.activeSwitch.setOnCheckedChangeListener { _, newActive -> - active = newActive - changeActive(newActive) - } - binding.activeSwitch.isChecked = active - } + abstract var volume: Float + abstract var muted: Boolean abstract fun startNote(note: Note) abstract fun stop() - abstract fun changeActive(newActive: Boolean) + abstract fun stopNote(note: Note) + abstract fun destroy() companion object { - val instruments = - mutableListOf( - MonoInstrument("Bass"), - PolyInstrument("Chords"), - ) - - val voice = mutableListOf( - BassVoice(instruments[0]), - ChordVoice(instruments[1]), - ) + val instruments = mutableListOf() } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt index dc64bcc..b4c68a4 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -10,33 +10,67 @@ package com.lukas.music.instruments +import com.lukas.music.song.note.Note + class InternalInstrument { private val id = createInstrument() - var active: Boolean = false - set(value) { - field = value - setInstrumentActive(id, value) - } + var note: Note? = null var waveform: Waveform = Waveform.SINE set(value) { field = value setInstrumentWaveform(id, value.id) - // this is to resend the setInstrumentActive for the new waveform in the internal c++ code - active = active + refresh() } - fun startNote(frequency: Double) { - startNote(id, frequency) + var volume: Float = 0.3f + set(value) { + field = value + if (!muted) { + actualVolume = value + } + } + + var muted: Boolean = false + set(value) { + field = value + actualVolume = if (value) 0.0f else volume + } + + private var actualVolume: Float = 1.0f + set(value) { + field = value + setVolume(id, value) + } + + init { + refresh() + } + + private fun refresh() { + // this is to resend the old information to the internal c++ code (when changing the waveform) + muted = muted + volume = volume + } + + fun startNote(note: Note) { + this.note = note + startNote(id, note.frequency) } fun endNote() { + note = null endNote(id) } + fun destroy() { + destroy(id) + } + private external fun createInstrument(): Int - private external fun setInstrumentActive(id: Int, isActive: Boolean) private external fun setInstrumentWaveform(id: Int, waveform: Int) private external fun startNote(id: Int, frequency: Double) private external fun endNote(id: Int) + private external fun setVolume(id: Int, volume: Float) + private external fun destroy(id: Int) } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt index e7b7464..2b43524 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -21,15 +21,36 @@ internalInstrument.waveform = value } - override fun startNote(note: Note) { - internalInstrument.startNote(note.frequency) - } + override var volume: Float = 1.0f + set(value) { + field = value + internalInstrument.volume = volume + } - override fun changeActive(newActive: Boolean) { - internalInstrument.active = newActive + override var muted: Boolean = false + set(value) { + field = value + internalInstrument.muted = value + } + + override fun startNote(note: Note) { + if (note == internalInstrument.note) { + return + } + internalInstrument.startNote(note) } override fun stop() { internalInstrument.endNote() } + + override fun stopNote(note: Note) { + if (note == internalInstrument.note) { + stop() + } + } + + override fun destroy() { + internalInstrument.destroy() + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt index 9d8b4f9..26e4fbb 100644 --- a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt @@ -24,27 +24,55 @@ } } + override var volume: Float = 1.0f + set(value) { + field = value + for (internalInstrument in internalInstruments) { + internalInstrument.volume = volume + } + } + + override var muted: Boolean = false + set(value) { + field = value + for (instrument in internalInstruments) { + instrument.muted = value + } + } + override fun startNote(note: Note) { for ((index, instrumentPlaying) in playing.withIndex()) { if (!instrumentPlaying) { - internalInstruments[index].startNote(note.frequency) + internalInstruments[index].startNote(note) playing[index] = true return } + if (internalInstruments[index].note == note) { + return + } } throw IllegalStateException("cannot start another note with the current amount of oscillators") } - override fun changeActive(newActive: Boolean) { - for (instrument in internalInstruments) { - instrument.active = newActive - } - } - override fun stop() { for ((i, instrument) in internalInstruments.withIndex()) { instrument.endNote() playing[i] = false } } + + override fun stopNote(note: Note) { + for ((i, instrument) in internalInstruments.withIndex()) { + if (instrument.note == note) { + instrument.endNote() + playing[i] = false + } + } + } + + override fun destroy() { + for (instrument in internalInstruments) { + instrument.destroy() + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/Song.kt b/app/src/main/java/com/lukas/music/song/Song.kt index a3123f2..fc5421b 100644 --- a/app/src/main/java/com/lukas/music/song/Song.kt +++ b/app/src/main/java/com/lukas/music/song/Song.kt @@ -16,10 +16,33 @@ import com.lukas.music.util.Cycle class Song( - var root: Note, + root: Note, val beats: Int ) : Cycle(beats) { val chordProgression = ChordProgression() + var soloInstrument: Instrument? = null + set(value) { + field = value + value?.let { + for (instrument in Instrument.instruments) { + if (instrument != value) { + instrument.stop() + } + } + } + } + + var root: Note = root + set(value) { + field = value + stopAllInstruments() + } + + private fun stopAllInstruments() { + for (instrument in Instrument.instruments) { + instrument.stop() + } + } init { for (i in 0 until beats) { @@ -27,6 +50,7 @@ } wraparoundListeners += { chordProgression.step() + stopAllInstruments() } } @@ -34,8 +58,12 @@ super.step() val chord = chordProgression.currentItem?.currentItem ?: return index val chordNotes = chord.getNotes(root) - for (voice in Instrument.voice) { - voice.step(root, chordNotes) + soloInstrument?.let { + it.voice.step(root, chordNotes, index) + } ?: run { + for (instrument in Instrument.instruments) { + instrument.voice.step(root, chordNotes, index) + } } return index } diff --git a/app/src/main/java/com/lukas/music/song/chords/Chord.kt b/app/src/main/java/com/lukas/music/song/chords/Chord.kt index db45fdd..4400dae 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Chord.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Chord.kt @@ -19,6 +19,12 @@ interval = Interval(value) } var interval = Interval(note) + set(value) { + field = value + if (note != value.distance) { + note = value.distance + } + } fun getNotes(root: Note): Array { return Array(chordType.notes.size) { root + note + chordType.notes[it] } diff --git a/app/src/main/java/com/lukas/music/song/chords/Interval.kt b/app/src/main/java/com/lukas/music/song/chords/Interval.kt index 2726787..d112805 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Interval.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Interval.kt @@ -10,7 +10,7 @@ package com.lukas.music.song.chords -class Interval(private val distance: Int) { +class Interval(val distance: Int) { val name: IntervalName = when (distance) { 0 -> IntervalName.UNISON 1, 2 -> IntervalName.SECOND @@ -32,7 +32,7 @@ return name.toString() } - enum class IntervalName(val distance: Int, val romanVersion: String) { + enum class IntervalName(private val distance: Int, val romanVersion: String) { UNISON(0, "I"), SECOND(1, "II"), THIRD(3, "III"), diff --git a/app/src/main/java/com/lukas/music/song/note/Note.kt b/app/src/main/java/com/lukas/music/song/note/Note.kt index d3a588d..17bfb2b 100644 --- a/app/src/main/java/com/lukas/music/song/note/Note.kt +++ b/app/src/main/java/com/lukas/music/song/note/Note.kt @@ -28,6 +28,22 @@ return this + (-other) } + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Note + + if (id != other.id) return false + + return true + } + + override fun hashCode(): Int { + return id + } + + companion object { val NOTES = Array(128) { Note(it) } diff --git a/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt b/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt index 3990554..4706068 100644 --- a/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt +++ b/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt @@ -14,9 +14,16 @@ import com.lukas.music.song.note.Note class BassVoice(instrument: Instrument) : Voice(instrument) { - override val steps = listOf(1, 3) + override var noteActive: Array> = arrayOf( + arrayOf(true), + arrayOf(false), + arrayOf(true), + arrayOf(false) + ) - override fun step(root: Note, chord: Array) { - instrument.startNote(chord[0] - 24) + override val noteCount: Int = 1 + + override fun getNotes(root: Note, chordNotes: Array): Array { + return arrayOf(chordNotes[0] - 24) } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/voice/ChordVoice.kt b/app/src/main/java/com/lukas/music/song/voice/ChordVoice.kt index fc5362d..ab7117f 100644 --- a/app/src/main/java/com/lukas/music/song/voice/ChordVoice.kt +++ b/app/src/main/java/com/lukas/music/song/voice/ChordVoice.kt @@ -14,12 +14,15 @@ import com.lukas.music.song.note.Note class ChordVoice(instrument: Instrument) : Voice(instrument) { - override val steps: List = listOf(2, 4) + override var noteActive: Array> = arrayOf( + Array(3) { false }, + Array(3) { true }, + Array(3) { false }, + Array(3) { true }, + ) + override val noteCount: Int = 3 - override fun step(root: Note, chord: Array) { - instrument.stop() - for (note in chord) { - instrument.startNote(note) - } + override fun getNotes(root: Note, chordNotes: Array): Array { + return chordNotes } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/voice/Voice.kt b/app/src/main/java/com/lukas/music/song/voice/Voice.kt index f7fca94..4f56c2a 100644 --- a/app/src/main/java/com/lukas/music/song/voice/Voice.kt +++ b/app/src/main/java/com/lukas/music/song/voice/Voice.kt @@ -12,9 +12,36 @@ import com.lukas.music.instruments.Instrument import com.lukas.music.song.note.Note +import kotlin.reflect.KClass abstract class Voice(val instrument: Instrument) { - abstract val steps: List + abstract var noteActive: Array> + abstract val noteCount: Int - abstract fun step(root: Note, chord: Array) + abstract fun getNotes(root: Note, chordNotes: Array): Array + + fun step(root: Note, chordNotes: Array, beat: Int) { + if (instrument.muted) { + return + } + val activeNotes = noteActive[beat] + val notes = getNotes(root, chordNotes) + for ((index, active) in activeNotes.withIndex()) { + val note = notes[index] + if (!active) { + instrument.stopNote(note) + continue + } + instrument.startNote(note) + } + } + + companion object { + val DEFAULT_VOICES = listOf>( + BassVoice::class, + ChordVoice::class, + ) + + val DEFAULT_VOICE_NAMES = listOf("Bass", "Chord") + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/adapters/InstrumentAdapter.kt b/app/src/main/java/com/lukas/music/ui/adapters/InstrumentAdapter.kt index 8961831..d51f62e 100644 --- a/app/src/main/java/com/lukas/music/ui/adapters/InstrumentAdapter.kt +++ b/app/src/main/java/com/lukas/music/ui/adapters/InstrumentAdapter.kt @@ -11,54 +11,26 @@ package com.lukas.music.ui.adapters import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup -import android.widget.AdapterView -import android.widget.ArrayAdapter import androidx.recyclerview.widget.RecyclerView import com.lukas.music.databinding.FragmentInstrumentBinding import com.lukas.music.instruments.Instrument -import com.lukas.music.instruments.Waveform +import com.lukas.music.ui.fragments.InstrumentListFragment -class InstrumentAdapter : RecyclerView.Adapter() { - class InstrumentViewHolder(val binding: FragmentInstrumentBinding) : - RecyclerView.ViewHolder(binding.root), AdapterView.OnItemSelectedListener { - lateinit var instrument: Instrument - - init { - val adapter = ArrayAdapter( - binding.root.context, - android.R.layout.simple_spinner_dropdown_item, Waveform.VALUES - ) - binding.waveformSelection.adapter = adapter - binding.waveformSelection.onItemSelectedListener = this - } - - override fun onItemSelected( - adapterView: AdapterView<*>?, - view: View?, - position: Int, - id: Long - ) { - instrument.waveform = Waveform.VALUES[position] - } - - override fun onNothingSelected(adapterView: AdapterView<*>?) { - } - } +class InstrumentAdapter(val parent: InstrumentListFragment) : + RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): InstrumentViewHolder { val context = parent.context val inflater = LayoutInflater.from(context) val binding = FragmentInstrumentBinding.inflate(inflater, parent, false) - return InstrumentViewHolder(binding) + return InstrumentViewHolder(binding, this.parent.childFragmentManager, this) } override fun onBindViewHolder(holder: InstrumentViewHolder, position: Int) { val instrument = Instrument.instruments[position] holder.instrument = instrument - instrument.applyToView(holder.binding) } override fun getItemCount(): Int { diff --git a/.idea/misc.xml b/.idea/misc.xml index 97f8bf5..3d886c4 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,6 +11,8 @@ + + diff --git a/app/build.gradle b/app/build.gradle index 35173c4..97ec2a0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -56,12 +56,12 @@ } dependencies { - implementation 'androidx.core:core-ktx:1.7.0' implementation 'androidx.appcompat:appcompat:1.3.0' implementation 'com.google.android.material:material:1.6.1' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'androidx.gridlayout:gridlayout:1.0.0' + implementation 'org.jetbrains.kotlin:kotlin-reflect:1.7.20-Beta' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index dc80bd1..8e98b44 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -15,6 +15,9 @@ } AudioHost *thiz = static_cast(userData); for (auto const &instrument: *thiz->instruments) { + if (!instrument) { + continue; + } instrument->render(buffer, sampleCount); } for (uint32_t i = 0; i < sampleCount; i++) { diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 6ebd2b0..5505a35 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -8,7 +8,7 @@ static AudioHost *audioHost; -template +template void *listGet(_InputIterator iterator, uint32_t n) { for (uint32_t i = 0; i < n; i++) { iterator++; @@ -16,6 +16,18 @@ return *iterator; } +Instrument *getInstrument(uint32_t id) { + return static_cast(listGet(audioHost->instruments->begin(), id)); +} + +template +void listSet(_InputIterator iterator, uint32_t n, void *value) { + for (uint32_t i = 0; i < n; i++) { + iterator++; + } + *iterator = static_cast(value); +} + extern "C" { JNIEXPORT void JNICALL @@ -23,7 +35,6 @@ audioHost = new AudioHost(); } -extern "C" JNIEXPORT jint JNICALL Java_com_lukas_music_instruments_InternalInstrument_createInstrument(JNIEnv *env, jobject thiz) { uint32_t result = audioHost->instruments->size(); @@ -32,43 +43,37 @@ return result; } -extern "C" -JNIEXPORT void JNICALL -Java_com_lukas_music_instruments_InternalInstrument_setInstrumentActive(JNIEnv *env, jobject thiz, - jint id, jboolean active) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->wave->amplitude = active ? 0.3 : 0.0; -} -} -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_startNote(JNIEnv *env, jobject thiz, jint id, jdouble frequency) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->startNote(frequency); + getInstrument(id)->startNote(frequency); } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_endNote(JNIEnv *env, jobject thiz, jint id) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->endNote(); + getInstrument(id)->endNote(); } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_ui_fragments_PlayFragment_setMasterVolume(JNIEnv *env, jobject thiz, jdouble volume) { audioHost->masterVolume = volume; } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setInstrumentWaveform(JNIEnv *env, jobject thiz, jint id, jint waveform) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->setWaveform(static_cast(waveform)); + getInstrument(id)->setWaveform(static_cast(waveform)); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, + jfloat volume) { + getInstrument(id)->wave->amplitude = volume; +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_destroy(JNIEnv *env, jobject thiz, jint id) { + listSet(audioHost->instruments->begin(), id, nullptr); + delete getInstrument(id); +} } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt new file mode 100644 index 0000000..56f21e6 --- /dev/null +++ b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2022 Lukas Eisenhauer + * + * This program 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, either version 3 of the License, or(at your option) any later version. + * + * This program 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 this program. If not, see . + */ + +package com.lukas.music + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TableRow +import androidx.core.view.setMargins +import androidx.fragment.app.DialogFragment +import com.google.android.material.button.MaterialButton +import com.lukas.music.databinding.FragmentEditVoiceBinding +import com.lukas.music.song.Song +import com.lukas.music.song.voice.Voice +import com.lukas.music.util.ArrayProperty +import com.lukas.music.util.setupToggle + +class EditVoiceFragment(private val voice: Voice) : DialogFragment() { + private lateinit var binding: FragmentEditVoiceBinding + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEditVoiceBinding.inflate(inflater) + for (row in 0 until voice.noteCount) { + val rowLayout = TableRow(binding.root.context) + for (column in 0 until Song.currentSong.beats) { + val button = MaterialButton(binding.root.context) + button.layoutParams = buttonLayout + button.setupToggle(ArrayProperty(voice.noteActive[column], row), R.color.blue) + rowLayout.addView(button) + } + binding.noteGrid.addView(rowLayout) + } + binding.noteGrid.isStretchAllColumns = true + binding.closeButton.setOnClickListener { + dismiss() + } + return binding.root + } + + override fun onStart() { + super.onStart() + dialog?.window?.setLayout( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + } + + companion object { + val buttonLayout = TableRow.LayoutParams(0, TableRow.LayoutParams.WRAP_CONTENT) + + init { + buttonLayout.setMargins(5) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Instrument.kt b/app/src/main/java/com/lukas/music/instruments/Instrument.kt index 8ad90c6..92a896c 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -10,42 +10,22 @@ package com.lukas.music.instruments -import com.lukas.music.databinding.FragmentInstrumentBinding import com.lukas.music.song.note.Note import com.lukas.music.song.voice.BassVoice -import com.lukas.music.song.voice.ChordVoice import com.lukas.music.song.voice.Voice -abstract class Instrument(private var name: String) { - private var active = false +abstract class Instrument(var name: String) { + var voice: Voice = BassVoice(this) abstract var waveform: Waveform - - fun applyToView(binding: FragmentInstrumentBinding) { - binding.instrumentNameText.text = name - binding.editInstrumentButton.setOnClickListener { - println("click instrument $name") - } - binding.activeSwitch.setOnCheckedChangeListener { _, newActive -> - active = newActive - changeActive(newActive) - } - binding.activeSwitch.isChecked = active - } + abstract var volume: Float + abstract var muted: Boolean abstract fun startNote(note: Note) abstract fun stop() - abstract fun changeActive(newActive: Boolean) + abstract fun stopNote(note: Note) + abstract fun destroy() companion object { - val instruments = - mutableListOf( - MonoInstrument("Bass"), - PolyInstrument("Chords"), - ) - - val voice = mutableListOf( - BassVoice(instruments[0]), - ChordVoice(instruments[1]), - ) + val instruments = mutableListOf() } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt index dc64bcc..b4c68a4 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -10,33 +10,67 @@ package com.lukas.music.instruments +import com.lukas.music.song.note.Note + class InternalInstrument { private val id = createInstrument() - var active: Boolean = false - set(value) { - field = value - setInstrumentActive(id, value) - } + var note: Note? = null var waveform: Waveform = Waveform.SINE set(value) { field = value setInstrumentWaveform(id, value.id) - // this is to resend the setInstrumentActive for the new waveform in the internal c++ code - active = active + refresh() } - fun startNote(frequency: Double) { - startNote(id, frequency) + var volume: Float = 0.3f + set(value) { + field = value + if (!muted) { + actualVolume = value + } + } + + var muted: Boolean = false + set(value) { + field = value + actualVolume = if (value) 0.0f else volume + } + + private var actualVolume: Float = 1.0f + set(value) { + field = value + setVolume(id, value) + } + + init { + refresh() + } + + private fun refresh() { + // this is to resend the old information to the internal c++ code (when changing the waveform) + muted = muted + volume = volume + } + + fun startNote(note: Note) { + this.note = note + startNote(id, note.frequency) } fun endNote() { + note = null endNote(id) } + fun destroy() { + destroy(id) + } + private external fun createInstrument(): Int - private external fun setInstrumentActive(id: Int, isActive: Boolean) private external fun setInstrumentWaveform(id: Int, waveform: Int) private external fun startNote(id: Int, frequency: Double) private external fun endNote(id: Int) + private external fun setVolume(id: Int, volume: Float) + private external fun destroy(id: Int) } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt index e7b7464..2b43524 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -21,15 +21,36 @@ internalInstrument.waveform = value } - override fun startNote(note: Note) { - internalInstrument.startNote(note.frequency) - } + override var volume: Float = 1.0f + set(value) { + field = value + internalInstrument.volume = volume + } - override fun changeActive(newActive: Boolean) { - internalInstrument.active = newActive + override var muted: Boolean = false + set(value) { + field = value + internalInstrument.muted = value + } + + override fun startNote(note: Note) { + if (note == internalInstrument.note) { + return + } + internalInstrument.startNote(note) } override fun stop() { internalInstrument.endNote() } + + override fun stopNote(note: Note) { + if (note == internalInstrument.note) { + stop() + } + } + + override fun destroy() { + internalInstrument.destroy() + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt index 9d8b4f9..26e4fbb 100644 --- a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt @@ -24,27 +24,55 @@ } } + override var volume: Float = 1.0f + set(value) { + field = value + for (internalInstrument in internalInstruments) { + internalInstrument.volume = volume + } + } + + override var muted: Boolean = false + set(value) { + field = value + for (instrument in internalInstruments) { + instrument.muted = value + } + } + override fun startNote(note: Note) { for ((index, instrumentPlaying) in playing.withIndex()) { if (!instrumentPlaying) { - internalInstruments[index].startNote(note.frequency) + internalInstruments[index].startNote(note) playing[index] = true return } + if (internalInstruments[index].note == note) { + return + } } throw IllegalStateException("cannot start another note with the current amount of oscillators") } - override fun changeActive(newActive: Boolean) { - for (instrument in internalInstruments) { - instrument.active = newActive - } - } - override fun stop() { for ((i, instrument) in internalInstruments.withIndex()) { instrument.endNote() playing[i] = false } } + + override fun stopNote(note: Note) { + for ((i, instrument) in internalInstruments.withIndex()) { + if (instrument.note == note) { + instrument.endNote() + playing[i] = false + } + } + } + + override fun destroy() { + for (instrument in internalInstruments) { + instrument.destroy() + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/Song.kt b/app/src/main/java/com/lukas/music/song/Song.kt index a3123f2..fc5421b 100644 --- a/app/src/main/java/com/lukas/music/song/Song.kt +++ b/app/src/main/java/com/lukas/music/song/Song.kt @@ -16,10 +16,33 @@ import com.lukas.music.util.Cycle class Song( - var root: Note, + root: Note, val beats: Int ) : Cycle(beats) { val chordProgression = ChordProgression() + var soloInstrument: Instrument? = null + set(value) { + field = value + value?.let { + for (instrument in Instrument.instruments) { + if (instrument != value) { + instrument.stop() + } + } + } + } + + var root: Note = root + set(value) { + field = value + stopAllInstruments() + } + + private fun stopAllInstruments() { + for (instrument in Instrument.instruments) { + instrument.stop() + } + } init { for (i in 0 until beats) { @@ -27,6 +50,7 @@ } wraparoundListeners += { chordProgression.step() + stopAllInstruments() } } @@ -34,8 +58,12 @@ super.step() val chord = chordProgression.currentItem?.currentItem ?: return index val chordNotes = chord.getNotes(root) - for (voice in Instrument.voice) { - voice.step(root, chordNotes) + soloInstrument?.let { + it.voice.step(root, chordNotes, index) + } ?: run { + for (instrument in Instrument.instruments) { + instrument.voice.step(root, chordNotes, index) + } } return index } diff --git a/app/src/main/java/com/lukas/music/song/chords/Chord.kt b/app/src/main/java/com/lukas/music/song/chords/Chord.kt index db45fdd..4400dae 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Chord.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Chord.kt @@ -19,6 +19,12 @@ interval = Interval(value) } var interval = Interval(note) + set(value) { + field = value + if (note != value.distance) { + note = value.distance + } + } fun getNotes(root: Note): Array { return Array(chordType.notes.size) { root + note + chordType.notes[it] } diff --git a/app/src/main/java/com/lukas/music/song/chords/Interval.kt b/app/src/main/java/com/lukas/music/song/chords/Interval.kt index 2726787..d112805 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Interval.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Interval.kt @@ -10,7 +10,7 @@ package com.lukas.music.song.chords -class Interval(private val distance: Int) { +class Interval(val distance: Int) { val name: IntervalName = when (distance) { 0 -> IntervalName.UNISON 1, 2 -> IntervalName.SECOND @@ -32,7 +32,7 @@ return name.toString() } - enum class IntervalName(val distance: Int, val romanVersion: String) { + enum class IntervalName(private val distance: Int, val romanVersion: String) { UNISON(0, "I"), SECOND(1, "II"), THIRD(3, "III"), diff --git a/app/src/main/java/com/lukas/music/song/note/Note.kt b/app/src/main/java/com/lukas/music/song/note/Note.kt index d3a588d..17bfb2b 100644 --- a/app/src/main/java/com/lukas/music/song/note/Note.kt +++ b/app/src/main/java/com/lukas/music/song/note/Note.kt @@ -28,6 +28,22 @@ return this + (-other) } + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Note + + if (id != other.id) return false + + return true + } + + override fun hashCode(): Int { + return id + } + + companion object { val NOTES = Array(128) { Note(it) } diff --git a/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt b/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt index 3990554..4706068 100644 --- a/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt +++ b/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt @@ -14,9 +14,16 @@ import com.lukas.music.song.note.Note class BassVoice(instrument: Instrument) : Voice(instrument) { - override val steps = listOf(1, 3) + override var noteActive: Array> = arrayOf( + arrayOf(true), + arrayOf(false), + arrayOf(true), + arrayOf(false) + ) - override fun step(root: Note, chord: Array) { - instrument.startNote(chord[0] - 24) + override val noteCount: Int = 1 + + override fun getNotes(root: Note, chordNotes: Array): Array { + return arrayOf(chordNotes[0] - 24) } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/voice/ChordVoice.kt b/app/src/main/java/com/lukas/music/song/voice/ChordVoice.kt index fc5362d..ab7117f 100644 --- a/app/src/main/java/com/lukas/music/song/voice/ChordVoice.kt +++ b/app/src/main/java/com/lukas/music/song/voice/ChordVoice.kt @@ -14,12 +14,15 @@ import com.lukas.music.song.note.Note class ChordVoice(instrument: Instrument) : Voice(instrument) { - override val steps: List = listOf(2, 4) + override var noteActive: Array> = arrayOf( + Array(3) { false }, + Array(3) { true }, + Array(3) { false }, + Array(3) { true }, + ) + override val noteCount: Int = 3 - override fun step(root: Note, chord: Array) { - instrument.stop() - for (note in chord) { - instrument.startNote(note) - } + override fun getNotes(root: Note, chordNotes: Array): Array { + return chordNotes } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/voice/Voice.kt b/app/src/main/java/com/lukas/music/song/voice/Voice.kt index f7fca94..4f56c2a 100644 --- a/app/src/main/java/com/lukas/music/song/voice/Voice.kt +++ b/app/src/main/java/com/lukas/music/song/voice/Voice.kt @@ -12,9 +12,36 @@ import com.lukas.music.instruments.Instrument import com.lukas.music.song.note.Note +import kotlin.reflect.KClass abstract class Voice(val instrument: Instrument) { - abstract val steps: List + abstract var noteActive: Array> + abstract val noteCount: Int - abstract fun step(root: Note, chord: Array) + abstract fun getNotes(root: Note, chordNotes: Array): Array + + fun step(root: Note, chordNotes: Array, beat: Int) { + if (instrument.muted) { + return + } + val activeNotes = noteActive[beat] + val notes = getNotes(root, chordNotes) + for ((index, active) in activeNotes.withIndex()) { + val note = notes[index] + if (!active) { + instrument.stopNote(note) + continue + } + instrument.startNote(note) + } + } + + companion object { + val DEFAULT_VOICES = listOf>( + BassVoice::class, + ChordVoice::class, + ) + + val DEFAULT_VOICE_NAMES = listOf("Bass", "Chord") + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/adapters/InstrumentAdapter.kt b/app/src/main/java/com/lukas/music/ui/adapters/InstrumentAdapter.kt index 8961831..d51f62e 100644 --- a/app/src/main/java/com/lukas/music/ui/adapters/InstrumentAdapter.kt +++ b/app/src/main/java/com/lukas/music/ui/adapters/InstrumentAdapter.kt @@ -11,54 +11,26 @@ package com.lukas.music.ui.adapters import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup -import android.widget.AdapterView -import android.widget.ArrayAdapter import androidx.recyclerview.widget.RecyclerView import com.lukas.music.databinding.FragmentInstrumentBinding import com.lukas.music.instruments.Instrument -import com.lukas.music.instruments.Waveform +import com.lukas.music.ui.fragments.InstrumentListFragment -class InstrumentAdapter : RecyclerView.Adapter() { - class InstrumentViewHolder(val binding: FragmentInstrumentBinding) : - RecyclerView.ViewHolder(binding.root), AdapterView.OnItemSelectedListener { - lateinit var instrument: Instrument - - init { - val adapter = ArrayAdapter( - binding.root.context, - android.R.layout.simple_spinner_dropdown_item, Waveform.VALUES - ) - binding.waveformSelection.adapter = adapter - binding.waveformSelection.onItemSelectedListener = this - } - - override fun onItemSelected( - adapterView: AdapterView<*>?, - view: View?, - position: Int, - id: Long - ) { - instrument.waveform = Waveform.VALUES[position] - } - - override fun onNothingSelected(adapterView: AdapterView<*>?) { - } - } +class InstrumentAdapter(val parent: InstrumentListFragment) : + RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): InstrumentViewHolder { val context = parent.context val inflater = LayoutInflater.from(context) val binding = FragmentInstrumentBinding.inflate(inflater, parent, false) - return InstrumentViewHolder(binding) + return InstrumentViewHolder(binding, this.parent.childFragmentManager, this) } override fun onBindViewHolder(holder: InstrumentViewHolder, position: Int) { val instrument = Instrument.instruments[position] holder.instrument = instrument - instrument.applyToView(holder.binding) } override fun getItemCount(): Int { diff --git a/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt b/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt new file mode 100644 index 0000000..3e129bc --- /dev/null +++ b/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2022 Lukas Eisenhauer + * + * This program 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, either version 3 of the License, or(at your option) any later version. + * + * This program 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 this program. If not, see . + */ + +package com.lukas.music.ui.adapters + +import androidx.fragment.app.FragmentManager +import androidx.recyclerview.widget.RecyclerView +import com.lukas.music.R +import com.lukas.music.databinding.FragmentInstrumentBinding +import com.lukas.music.instruments.Instrument +import com.lukas.music.song.Song +import com.lukas.music.ui.fragments.EditInstrumentFragment +import com.lukas.music.util.setupToggle +import com.lukas.music.util.updateToggle + +class InstrumentViewHolder( + val binding: FragmentInstrumentBinding, + private val childFragmentManager: FragmentManager, + private val adapter: InstrumentAdapter +) : + RecyclerView.ViewHolder(binding.root) { + init { + HOLDERS += this + } + + var solo: Boolean = false + set(value) { + if (value) { + for (holder in HOLDERS) { + holder.solo = false + } + Song.currentSong.soloInstrument = instrument + } + field = value + binding.soloButton.updateToggle(this::solo, R.color.blue) + } + + var instrument: Instrument? = null + set(value) { + field = value + value ?: return + binding.instrumentNameText.text = instrument?.name + binding.editInstrumentButton.setOnClickListener { + EditInstrumentFragment(instrument!!, this).showNow(childFragmentManager, "") + } + binding.muteButton.setupToggle(instrument!!::muted, R.color.red) + binding.soloButton.setupToggle(this::solo, R.color.blue) { + if (!it) { + Song.currentSong.soloInstrument = null + } + } + binding.deleteButton.setOnClickListener { + val index = Instrument.instruments.indexOf(instrument) + Instrument.instruments -= instrument!! + adapter.notifyItemRemoved(index) + instrument!!.destroy() + } + } + + companion object { + val HOLDERS = mutableListOf() + } +} \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 97f8bf5..3d886c4 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,6 +11,8 @@ + + diff --git a/app/build.gradle b/app/build.gradle index 35173c4..97ec2a0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -56,12 +56,12 @@ } dependencies { - implementation 'androidx.core:core-ktx:1.7.0' implementation 'androidx.appcompat:appcompat:1.3.0' implementation 'com.google.android.material:material:1.6.1' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'androidx.gridlayout:gridlayout:1.0.0' + implementation 'org.jetbrains.kotlin:kotlin-reflect:1.7.20-Beta' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index dc80bd1..8e98b44 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -15,6 +15,9 @@ } AudioHost *thiz = static_cast(userData); for (auto const &instrument: *thiz->instruments) { + if (!instrument) { + continue; + } instrument->render(buffer, sampleCount); } for (uint32_t i = 0; i < sampleCount; i++) { diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 6ebd2b0..5505a35 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -8,7 +8,7 @@ static AudioHost *audioHost; -template +template void *listGet(_InputIterator iterator, uint32_t n) { for (uint32_t i = 0; i < n; i++) { iterator++; @@ -16,6 +16,18 @@ return *iterator; } +Instrument *getInstrument(uint32_t id) { + return static_cast(listGet(audioHost->instruments->begin(), id)); +} + +template +void listSet(_InputIterator iterator, uint32_t n, void *value) { + for (uint32_t i = 0; i < n; i++) { + iterator++; + } + *iterator = static_cast(value); +} + extern "C" { JNIEXPORT void JNICALL @@ -23,7 +35,6 @@ audioHost = new AudioHost(); } -extern "C" JNIEXPORT jint JNICALL Java_com_lukas_music_instruments_InternalInstrument_createInstrument(JNIEnv *env, jobject thiz) { uint32_t result = audioHost->instruments->size(); @@ -32,43 +43,37 @@ return result; } -extern "C" -JNIEXPORT void JNICALL -Java_com_lukas_music_instruments_InternalInstrument_setInstrumentActive(JNIEnv *env, jobject thiz, - jint id, jboolean active) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->wave->amplitude = active ? 0.3 : 0.0; -} -} -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_startNote(JNIEnv *env, jobject thiz, jint id, jdouble frequency) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->startNote(frequency); + getInstrument(id)->startNote(frequency); } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_endNote(JNIEnv *env, jobject thiz, jint id) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->endNote(); + getInstrument(id)->endNote(); } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_ui_fragments_PlayFragment_setMasterVolume(JNIEnv *env, jobject thiz, jdouble volume) { audioHost->masterVolume = volume; } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setInstrumentWaveform(JNIEnv *env, jobject thiz, jint id, jint waveform) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->setWaveform(static_cast(waveform)); + getInstrument(id)->setWaveform(static_cast(waveform)); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, + jfloat volume) { + getInstrument(id)->wave->amplitude = volume; +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_destroy(JNIEnv *env, jobject thiz, jint id) { + listSet(audioHost->instruments->begin(), id, nullptr); + delete getInstrument(id); +} } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt new file mode 100644 index 0000000..56f21e6 --- /dev/null +++ b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2022 Lukas Eisenhauer + * + * This program 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, either version 3 of the License, or(at your option) any later version. + * + * This program 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 this program. If not, see . + */ + +package com.lukas.music + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TableRow +import androidx.core.view.setMargins +import androidx.fragment.app.DialogFragment +import com.google.android.material.button.MaterialButton +import com.lukas.music.databinding.FragmentEditVoiceBinding +import com.lukas.music.song.Song +import com.lukas.music.song.voice.Voice +import com.lukas.music.util.ArrayProperty +import com.lukas.music.util.setupToggle + +class EditVoiceFragment(private val voice: Voice) : DialogFragment() { + private lateinit var binding: FragmentEditVoiceBinding + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEditVoiceBinding.inflate(inflater) + for (row in 0 until voice.noteCount) { + val rowLayout = TableRow(binding.root.context) + for (column in 0 until Song.currentSong.beats) { + val button = MaterialButton(binding.root.context) + button.layoutParams = buttonLayout + button.setupToggle(ArrayProperty(voice.noteActive[column], row), R.color.blue) + rowLayout.addView(button) + } + binding.noteGrid.addView(rowLayout) + } + binding.noteGrid.isStretchAllColumns = true + binding.closeButton.setOnClickListener { + dismiss() + } + return binding.root + } + + override fun onStart() { + super.onStart() + dialog?.window?.setLayout( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + } + + companion object { + val buttonLayout = TableRow.LayoutParams(0, TableRow.LayoutParams.WRAP_CONTENT) + + init { + buttonLayout.setMargins(5) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Instrument.kt b/app/src/main/java/com/lukas/music/instruments/Instrument.kt index 8ad90c6..92a896c 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -10,42 +10,22 @@ package com.lukas.music.instruments -import com.lukas.music.databinding.FragmentInstrumentBinding import com.lukas.music.song.note.Note import com.lukas.music.song.voice.BassVoice -import com.lukas.music.song.voice.ChordVoice import com.lukas.music.song.voice.Voice -abstract class Instrument(private var name: String) { - private var active = false +abstract class Instrument(var name: String) { + var voice: Voice = BassVoice(this) abstract var waveform: Waveform - - fun applyToView(binding: FragmentInstrumentBinding) { - binding.instrumentNameText.text = name - binding.editInstrumentButton.setOnClickListener { - println("click instrument $name") - } - binding.activeSwitch.setOnCheckedChangeListener { _, newActive -> - active = newActive - changeActive(newActive) - } - binding.activeSwitch.isChecked = active - } + abstract var volume: Float + abstract var muted: Boolean abstract fun startNote(note: Note) abstract fun stop() - abstract fun changeActive(newActive: Boolean) + abstract fun stopNote(note: Note) + abstract fun destroy() companion object { - val instruments = - mutableListOf( - MonoInstrument("Bass"), - PolyInstrument("Chords"), - ) - - val voice = mutableListOf( - BassVoice(instruments[0]), - ChordVoice(instruments[1]), - ) + val instruments = mutableListOf() } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt index dc64bcc..b4c68a4 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -10,33 +10,67 @@ package com.lukas.music.instruments +import com.lukas.music.song.note.Note + class InternalInstrument { private val id = createInstrument() - var active: Boolean = false - set(value) { - field = value - setInstrumentActive(id, value) - } + var note: Note? = null var waveform: Waveform = Waveform.SINE set(value) { field = value setInstrumentWaveform(id, value.id) - // this is to resend the setInstrumentActive for the new waveform in the internal c++ code - active = active + refresh() } - fun startNote(frequency: Double) { - startNote(id, frequency) + var volume: Float = 0.3f + set(value) { + field = value + if (!muted) { + actualVolume = value + } + } + + var muted: Boolean = false + set(value) { + field = value + actualVolume = if (value) 0.0f else volume + } + + private var actualVolume: Float = 1.0f + set(value) { + field = value + setVolume(id, value) + } + + init { + refresh() + } + + private fun refresh() { + // this is to resend the old information to the internal c++ code (when changing the waveform) + muted = muted + volume = volume + } + + fun startNote(note: Note) { + this.note = note + startNote(id, note.frequency) } fun endNote() { + note = null endNote(id) } + fun destroy() { + destroy(id) + } + private external fun createInstrument(): Int - private external fun setInstrumentActive(id: Int, isActive: Boolean) private external fun setInstrumentWaveform(id: Int, waveform: Int) private external fun startNote(id: Int, frequency: Double) private external fun endNote(id: Int) + private external fun setVolume(id: Int, volume: Float) + private external fun destroy(id: Int) } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt index e7b7464..2b43524 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -21,15 +21,36 @@ internalInstrument.waveform = value } - override fun startNote(note: Note) { - internalInstrument.startNote(note.frequency) - } + override var volume: Float = 1.0f + set(value) { + field = value + internalInstrument.volume = volume + } - override fun changeActive(newActive: Boolean) { - internalInstrument.active = newActive + override var muted: Boolean = false + set(value) { + field = value + internalInstrument.muted = value + } + + override fun startNote(note: Note) { + if (note == internalInstrument.note) { + return + } + internalInstrument.startNote(note) } override fun stop() { internalInstrument.endNote() } + + override fun stopNote(note: Note) { + if (note == internalInstrument.note) { + stop() + } + } + + override fun destroy() { + internalInstrument.destroy() + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt index 9d8b4f9..26e4fbb 100644 --- a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt @@ -24,27 +24,55 @@ } } + override var volume: Float = 1.0f + set(value) { + field = value + for (internalInstrument in internalInstruments) { + internalInstrument.volume = volume + } + } + + override var muted: Boolean = false + set(value) { + field = value + for (instrument in internalInstruments) { + instrument.muted = value + } + } + override fun startNote(note: Note) { for ((index, instrumentPlaying) in playing.withIndex()) { if (!instrumentPlaying) { - internalInstruments[index].startNote(note.frequency) + internalInstruments[index].startNote(note) playing[index] = true return } + if (internalInstruments[index].note == note) { + return + } } throw IllegalStateException("cannot start another note with the current amount of oscillators") } - override fun changeActive(newActive: Boolean) { - for (instrument in internalInstruments) { - instrument.active = newActive - } - } - override fun stop() { for ((i, instrument) in internalInstruments.withIndex()) { instrument.endNote() playing[i] = false } } + + override fun stopNote(note: Note) { + for ((i, instrument) in internalInstruments.withIndex()) { + if (instrument.note == note) { + instrument.endNote() + playing[i] = false + } + } + } + + override fun destroy() { + for (instrument in internalInstruments) { + instrument.destroy() + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/Song.kt b/app/src/main/java/com/lukas/music/song/Song.kt index a3123f2..fc5421b 100644 --- a/app/src/main/java/com/lukas/music/song/Song.kt +++ b/app/src/main/java/com/lukas/music/song/Song.kt @@ -16,10 +16,33 @@ import com.lukas.music.util.Cycle class Song( - var root: Note, + root: Note, val beats: Int ) : Cycle(beats) { val chordProgression = ChordProgression() + var soloInstrument: Instrument? = null + set(value) { + field = value + value?.let { + for (instrument in Instrument.instruments) { + if (instrument != value) { + instrument.stop() + } + } + } + } + + var root: Note = root + set(value) { + field = value + stopAllInstruments() + } + + private fun stopAllInstruments() { + for (instrument in Instrument.instruments) { + instrument.stop() + } + } init { for (i in 0 until beats) { @@ -27,6 +50,7 @@ } wraparoundListeners += { chordProgression.step() + stopAllInstruments() } } @@ -34,8 +58,12 @@ super.step() val chord = chordProgression.currentItem?.currentItem ?: return index val chordNotes = chord.getNotes(root) - for (voice in Instrument.voice) { - voice.step(root, chordNotes) + soloInstrument?.let { + it.voice.step(root, chordNotes, index) + } ?: run { + for (instrument in Instrument.instruments) { + instrument.voice.step(root, chordNotes, index) + } } return index } diff --git a/app/src/main/java/com/lukas/music/song/chords/Chord.kt b/app/src/main/java/com/lukas/music/song/chords/Chord.kt index db45fdd..4400dae 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Chord.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Chord.kt @@ -19,6 +19,12 @@ interval = Interval(value) } var interval = Interval(note) + set(value) { + field = value + if (note != value.distance) { + note = value.distance + } + } fun getNotes(root: Note): Array { return Array(chordType.notes.size) { root + note + chordType.notes[it] } diff --git a/app/src/main/java/com/lukas/music/song/chords/Interval.kt b/app/src/main/java/com/lukas/music/song/chords/Interval.kt index 2726787..d112805 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Interval.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Interval.kt @@ -10,7 +10,7 @@ package com.lukas.music.song.chords -class Interval(private val distance: Int) { +class Interval(val distance: Int) { val name: IntervalName = when (distance) { 0 -> IntervalName.UNISON 1, 2 -> IntervalName.SECOND @@ -32,7 +32,7 @@ return name.toString() } - enum class IntervalName(val distance: Int, val romanVersion: String) { + enum class IntervalName(private val distance: Int, val romanVersion: String) { UNISON(0, "I"), SECOND(1, "II"), THIRD(3, "III"), diff --git a/app/src/main/java/com/lukas/music/song/note/Note.kt b/app/src/main/java/com/lukas/music/song/note/Note.kt index d3a588d..17bfb2b 100644 --- a/app/src/main/java/com/lukas/music/song/note/Note.kt +++ b/app/src/main/java/com/lukas/music/song/note/Note.kt @@ -28,6 +28,22 @@ return this + (-other) } + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Note + + if (id != other.id) return false + + return true + } + + override fun hashCode(): Int { + return id + } + + companion object { val NOTES = Array(128) { Note(it) } diff --git a/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt b/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt index 3990554..4706068 100644 --- a/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt +++ b/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt @@ -14,9 +14,16 @@ import com.lukas.music.song.note.Note class BassVoice(instrument: Instrument) : Voice(instrument) { - override val steps = listOf(1, 3) + override var noteActive: Array> = arrayOf( + arrayOf(true), + arrayOf(false), + arrayOf(true), + arrayOf(false) + ) - override fun step(root: Note, chord: Array) { - instrument.startNote(chord[0] - 24) + override val noteCount: Int = 1 + + override fun getNotes(root: Note, chordNotes: Array): Array { + return arrayOf(chordNotes[0] - 24) } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/voice/ChordVoice.kt b/app/src/main/java/com/lukas/music/song/voice/ChordVoice.kt index fc5362d..ab7117f 100644 --- a/app/src/main/java/com/lukas/music/song/voice/ChordVoice.kt +++ b/app/src/main/java/com/lukas/music/song/voice/ChordVoice.kt @@ -14,12 +14,15 @@ import com.lukas.music.song.note.Note class ChordVoice(instrument: Instrument) : Voice(instrument) { - override val steps: List = listOf(2, 4) + override var noteActive: Array> = arrayOf( + Array(3) { false }, + Array(3) { true }, + Array(3) { false }, + Array(3) { true }, + ) + override val noteCount: Int = 3 - override fun step(root: Note, chord: Array) { - instrument.stop() - for (note in chord) { - instrument.startNote(note) - } + override fun getNotes(root: Note, chordNotes: Array): Array { + return chordNotes } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/voice/Voice.kt b/app/src/main/java/com/lukas/music/song/voice/Voice.kt index f7fca94..4f56c2a 100644 --- a/app/src/main/java/com/lukas/music/song/voice/Voice.kt +++ b/app/src/main/java/com/lukas/music/song/voice/Voice.kt @@ -12,9 +12,36 @@ import com.lukas.music.instruments.Instrument import com.lukas.music.song.note.Note +import kotlin.reflect.KClass abstract class Voice(val instrument: Instrument) { - abstract val steps: List + abstract var noteActive: Array> + abstract val noteCount: Int - abstract fun step(root: Note, chord: Array) + abstract fun getNotes(root: Note, chordNotes: Array): Array + + fun step(root: Note, chordNotes: Array, beat: Int) { + if (instrument.muted) { + return + } + val activeNotes = noteActive[beat] + val notes = getNotes(root, chordNotes) + for ((index, active) in activeNotes.withIndex()) { + val note = notes[index] + if (!active) { + instrument.stopNote(note) + continue + } + instrument.startNote(note) + } + } + + companion object { + val DEFAULT_VOICES = listOf>( + BassVoice::class, + ChordVoice::class, + ) + + val DEFAULT_VOICE_NAMES = listOf("Bass", "Chord") + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/adapters/InstrumentAdapter.kt b/app/src/main/java/com/lukas/music/ui/adapters/InstrumentAdapter.kt index 8961831..d51f62e 100644 --- a/app/src/main/java/com/lukas/music/ui/adapters/InstrumentAdapter.kt +++ b/app/src/main/java/com/lukas/music/ui/adapters/InstrumentAdapter.kt @@ -11,54 +11,26 @@ package com.lukas.music.ui.adapters import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup -import android.widget.AdapterView -import android.widget.ArrayAdapter import androidx.recyclerview.widget.RecyclerView import com.lukas.music.databinding.FragmentInstrumentBinding import com.lukas.music.instruments.Instrument -import com.lukas.music.instruments.Waveform +import com.lukas.music.ui.fragments.InstrumentListFragment -class InstrumentAdapter : RecyclerView.Adapter() { - class InstrumentViewHolder(val binding: FragmentInstrumentBinding) : - RecyclerView.ViewHolder(binding.root), AdapterView.OnItemSelectedListener { - lateinit var instrument: Instrument - - init { - val adapter = ArrayAdapter( - binding.root.context, - android.R.layout.simple_spinner_dropdown_item, Waveform.VALUES - ) - binding.waveformSelection.adapter = adapter - binding.waveformSelection.onItemSelectedListener = this - } - - override fun onItemSelected( - adapterView: AdapterView<*>?, - view: View?, - position: Int, - id: Long - ) { - instrument.waveform = Waveform.VALUES[position] - } - - override fun onNothingSelected(adapterView: AdapterView<*>?) { - } - } +class InstrumentAdapter(val parent: InstrumentListFragment) : + RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): InstrumentViewHolder { val context = parent.context val inflater = LayoutInflater.from(context) val binding = FragmentInstrumentBinding.inflate(inflater, parent, false) - return InstrumentViewHolder(binding) + return InstrumentViewHolder(binding, this.parent.childFragmentManager, this) } override fun onBindViewHolder(holder: InstrumentViewHolder, position: Int) { val instrument = Instrument.instruments[position] holder.instrument = instrument - instrument.applyToView(holder.binding) } override fun getItemCount(): Int { diff --git a/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt b/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt new file mode 100644 index 0000000..3e129bc --- /dev/null +++ b/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2022 Lukas Eisenhauer + * + * This program 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, either version 3 of the License, or(at your option) any later version. + * + * This program 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 this program. If not, see . + */ + +package com.lukas.music.ui.adapters + +import androidx.fragment.app.FragmentManager +import androidx.recyclerview.widget.RecyclerView +import com.lukas.music.R +import com.lukas.music.databinding.FragmentInstrumentBinding +import com.lukas.music.instruments.Instrument +import com.lukas.music.song.Song +import com.lukas.music.ui.fragments.EditInstrumentFragment +import com.lukas.music.util.setupToggle +import com.lukas.music.util.updateToggle + +class InstrumentViewHolder( + val binding: FragmentInstrumentBinding, + private val childFragmentManager: FragmentManager, + private val adapter: InstrumentAdapter +) : + RecyclerView.ViewHolder(binding.root) { + init { + HOLDERS += this + } + + var solo: Boolean = false + set(value) { + if (value) { + for (holder in HOLDERS) { + holder.solo = false + } + Song.currentSong.soloInstrument = instrument + } + field = value + binding.soloButton.updateToggle(this::solo, R.color.blue) + } + + var instrument: Instrument? = null + set(value) { + field = value + value ?: return + binding.instrumentNameText.text = instrument?.name + binding.editInstrumentButton.setOnClickListener { + EditInstrumentFragment(instrument!!, this).showNow(childFragmentManager, "") + } + binding.muteButton.setupToggle(instrument!!::muted, R.color.red) + binding.soloButton.setupToggle(this::solo, R.color.blue) { + if (!it) { + Song.currentSong.soloInstrument = null + } + } + binding.deleteButton.setOnClickListener { + val index = Instrument.instruments.indexOf(instrument) + Instrument.instruments -= instrument!! + adapter.notifyItemRemoved(index) + instrument!!.destroy() + } + } + + companion object { + val HOLDERS = mutableListOf() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/fragments/EditChordFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/EditChordFragment.kt index a311a6c..bd7a3d9 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/EditChordFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/EditChordFragment.kt @@ -10,13 +10,10 @@ package com.lukas.music.ui.fragments -import android.R import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.AdapterView -import android.widget.ArrayAdapter import androidx.fragment.app.DialogFragment import com.lukas.music.databinding.FragmentEditChordBinding import com.lukas.music.song.Scale @@ -24,8 +21,10 @@ import com.lukas.music.song.chords.Chord import com.lukas.music.song.chords.ChordType import com.lukas.music.song.chords.Interval +import com.lukas.music.util.setup -class EditChordFragment(val chord: Chord, val songFragment: SongFragment) : DialogFragment() { +class EditChordFragment(val chord: Chord, private val songFragment: SongFragment) : + DialogFragment() { lateinit var binding: FragmentEditChordBinding override fun onCreateView( @@ -45,24 +44,12 @@ val pitches = if (songFragment.displayChordNames) { Array(Scale.MAJOR.steps.size) { (Song.currentSong.root + Scale.MAJOR.steps[it]).noteName.toString() } } else Interval.IntervalName.NAMES - val pitchAdapter = ArrayAdapter( - binding.root.context, - R.layout.simple_spinner_dropdown_item, pitches - ) - binding.pitchSpinner.adapter = pitchAdapter - binding.pitchSpinner.setSelection(chord.interval.name.ordinal) - binding.pitchSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { - override fun onItemSelected( - parent: AdapterView<*>?, - view: View?, - position: Int, - id: Long - ) { - chord.note = Scale.MAJOR.steps[position] - songFragment.updateChords() + binding.pitchSpinner.setup(pitches, chord.interval.name.ordinal) { + chord.note = Scale.MAJOR.steps[it] + if (binding.typeSpinner.selectedItemPosition == 0) { + chord.chordType = Scale.MAJOR.chordTypes[chord.interval.name.ordinal] } - - override fun onNothingSelected(parent: AdapterView<*>?) {} + songFragment.updateChords() } } @@ -71,32 +58,17 @@ for (chordType in ChordType.VALUES) { values += chordType.toString() } - val modifierAdapter = ArrayAdapter( - binding.root.context, - R.layout.simple_spinner_dropdown_item, values - ) - binding.typeSpinner.adapter = modifierAdapter - binding.typeSpinner.setSelection( - if (chord.chordType == Scale.MAJOR.chordTypes[chord.interval.name.ordinal]) - 0 + binding.typeSpinner.setup( + values, + if (chord.chordType == Scale.MAJOR.chordTypes[chord.interval.name.ordinal]) 0 else chord.chordType.ordinal + 1 - ) - binding.typeSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { - override fun onItemSelected( - parent: AdapterView<*>?, - view: View?, - position: Int, - id: Long - ) { - if (position == 0) { - chord.chordType = Scale.MAJOR.chordTypes[chord.interval.name.ordinal] - } else { - chord.chordType = ChordType.VALUES[position - 1] - } - songFragment.updateChords() + ) { + if (it == 0) { + chord.chordType = Scale.MAJOR.chordTypes[chord.interval.name.ordinal] + } else { + chord.chordType = ChordType.VALUES[it - 1] } - - override fun onNothingSelected(parent: AdapterView<*>?) {} + songFragment.updateChords() } } } \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 97f8bf5..3d886c4 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,6 +11,8 @@ + + diff --git a/app/build.gradle b/app/build.gradle index 35173c4..97ec2a0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -56,12 +56,12 @@ } dependencies { - implementation 'androidx.core:core-ktx:1.7.0' implementation 'androidx.appcompat:appcompat:1.3.0' implementation 'com.google.android.material:material:1.6.1' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'androidx.gridlayout:gridlayout:1.0.0' + implementation 'org.jetbrains.kotlin:kotlin-reflect:1.7.20-Beta' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index dc80bd1..8e98b44 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -15,6 +15,9 @@ } AudioHost *thiz = static_cast(userData); for (auto const &instrument: *thiz->instruments) { + if (!instrument) { + continue; + } instrument->render(buffer, sampleCount); } for (uint32_t i = 0; i < sampleCount; i++) { diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 6ebd2b0..5505a35 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -8,7 +8,7 @@ static AudioHost *audioHost; -template +template void *listGet(_InputIterator iterator, uint32_t n) { for (uint32_t i = 0; i < n; i++) { iterator++; @@ -16,6 +16,18 @@ return *iterator; } +Instrument *getInstrument(uint32_t id) { + return static_cast(listGet(audioHost->instruments->begin(), id)); +} + +template +void listSet(_InputIterator iterator, uint32_t n, void *value) { + for (uint32_t i = 0; i < n; i++) { + iterator++; + } + *iterator = static_cast(value); +} + extern "C" { JNIEXPORT void JNICALL @@ -23,7 +35,6 @@ audioHost = new AudioHost(); } -extern "C" JNIEXPORT jint JNICALL Java_com_lukas_music_instruments_InternalInstrument_createInstrument(JNIEnv *env, jobject thiz) { uint32_t result = audioHost->instruments->size(); @@ -32,43 +43,37 @@ return result; } -extern "C" -JNIEXPORT void JNICALL -Java_com_lukas_music_instruments_InternalInstrument_setInstrumentActive(JNIEnv *env, jobject thiz, - jint id, jboolean active) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->wave->amplitude = active ? 0.3 : 0.0; -} -} -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_startNote(JNIEnv *env, jobject thiz, jint id, jdouble frequency) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->startNote(frequency); + getInstrument(id)->startNote(frequency); } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_endNote(JNIEnv *env, jobject thiz, jint id) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->endNote(); + getInstrument(id)->endNote(); } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_ui_fragments_PlayFragment_setMasterVolume(JNIEnv *env, jobject thiz, jdouble volume) { audioHost->masterVolume = volume; } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setInstrumentWaveform(JNIEnv *env, jobject thiz, jint id, jint waveform) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->setWaveform(static_cast(waveform)); + getInstrument(id)->setWaveform(static_cast(waveform)); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, + jfloat volume) { + getInstrument(id)->wave->amplitude = volume; +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_destroy(JNIEnv *env, jobject thiz, jint id) { + listSet(audioHost->instruments->begin(), id, nullptr); + delete getInstrument(id); +} } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt new file mode 100644 index 0000000..56f21e6 --- /dev/null +++ b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2022 Lukas Eisenhauer + * + * This program 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, either version 3 of the License, or(at your option) any later version. + * + * This program 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 this program. If not, see . + */ + +package com.lukas.music + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TableRow +import androidx.core.view.setMargins +import androidx.fragment.app.DialogFragment +import com.google.android.material.button.MaterialButton +import com.lukas.music.databinding.FragmentEditVoiceBinding +import com.lukas.music.song.Song +import com.lukas.music.song.voice.Voice +import com.lukas.music.util.ArrayProperty +import com.lukas.music.util.setupToggle + +class EditVoiceFragment(private val voice: Voice) : DialogFragment() { + private lateinit var binding: FragmentEditVoiceBinding + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEditVoiceBinding.inflate(inflater) + for (row in 0 until voice.noteCount) { + val rowLayout = TableRow(binding.root.context) + for (column in 0 until Song.currentSong.beats) { + val button = MaterialButton(binding.root.context) + button.layoutParams = buttonLayout + button.setupToggle(ArrayProperty(voice.noteActive[column], row), R.color.blue) + rowLayout.addView(button) + } + binding.noteGrid.addView(rowLayout) + } + binding.noteGrid.isStretchAllColumns = true + binding.closeButton.setOnClickListener { + dismiss() + } + return binding.root + } + + override fun onStart() { + super.onStart() + dialog?.window?.setLayout( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + } + + companion object { + val buttonLayout = TableRow.LayoutParams(0, TableRow.LayoutParams.WRAP_CONTENT) + + init { + buttonLayout.setMargins(5) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Instrument.kt b/app/src/main/java/com/lukas/music/instruments/Instrument.kt index 8ad90c6..92a896c 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -10,42 +10,22 @@ package com.lukas.music.instruments -import com.lukas.music.databinding.FragmentInstrumentBinding import com.lukas.music.song.note.Note import com.lukas.music.song.voice.BassVoice -import com.lukas.music.song.voice.ChordVoice import com.lukas.music.song.voice.Voice -abstract class Instrument(private var name: String) { - private var active = false +abstract class Instrument(var name: String) { + var voice: Voice = BassVoice(this) abstract var waveform: Waveform - - fun applyToView(binding: FragmentInstrumentBinding) { - binding.instrumentNameText.text = name - binding.editInstrumentButton.setOnClickListener { - println("click instrument $name") - } - binding.activeSwitch.setOnCheckedChangeListener { _, newActive -> - active = newActive - changeActive(newActive) - } - binding.activeSwitch.isChecked = active - } + abstract var volume: Float + abstract var muted: Boolean abstract fun startNote(note: Note) abstract fun stop() - abstract fun changeActive(newActive: Boolean) + abstract fun stopNote(note: Note) + abstract fun destroy() companion object { - val instruments = - mutableListOf( - MonoInstrument("Bass"), - PolyInstrument("Chords"), - ) - - val voice = mutableListOf( - BassVoice(instruments[0]), - ChordVoice(instruments[1]), - ) + val instruments = mutableListOf() } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt index dc64bcc..b4c68a4 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -10,33 +10,67 @@ package com.lukas.music.instruments +import com.lukas.music.song.note.Note + class InternalInstrument { private val id = createInstrument() - var active: Boolean = false - set(value) { - field = value - setInstrumentActive(id, value) - } + var note: Note? = null var waveform: Waveform = Waveform.SINE set(value) { field = value setInstrumentWaveform(id, value.id) - // this is to resend the setInstrumentActive for the new waveform in the internal c++ code - active = active + refresh() } - fun startNote(frequency: Double) { - startNote(id, frequency) + var volume: Float = 0.3f + set(value) { + field = value + if (!muted) { + actualVolume = value + } + } + + var muted: Boolean = false + set(value) { + field = value + actualVolume = if (value) 0.0f else volume + } + + private var actualVolume: Float = 1.0f + set(value) { + field = value + setVolume(id, value) + } + + init { + refresh() + } + + private fun refresh() { + // this is to resend the old information to the internal c++ code (when changing the waveform) + muted = muted + volume = volume + } + + fun startNote(note: Note) { + this.note = note + startNote(id, note.frequency) } fun endNote() { + note = null endNote(id) } + fun destroy() { + destroy(id) + } + private external fun createInstrument(): Int - private external fun setInstrumentActive(id: Int, isActive: Boolean) private external fun setInstrumentWaveform(id: Int, waveform: Int) private external fun startNote(id: Int, frequency: Double) private external fun endNote(id: Int) + private external fun setVolume(id: Int, volume: Float) + private external fun destroy(id: Int) } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt index e7b7464..2b43524 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -21,15 +21,36 @@ internalInstrument.waveform = value } - override fun startNote(note: Note) { - internalInstrument.startNote(note.frequency) - } + override var volume: Float = 1.0f + set(value) { + field = value + internalInstrument.volume = volume + } - override fun changeActive(newActive: Boolean) { - internalInstrument.active = newActive + override var muted: Boolean = false + set(value) { + field = value + internalInstrument.muted = value + } + + override fun startNote(note: Note) { + if (note == internalInstrument.note) { + return + } + internalInstrument.startNote(note) } override fun stop() { internalInstrument.endNote() } + + override fun stopNote(note: Note) { + if (note == internalInstrument.note) { + stop() + } + } + + override fun destroy() { + internalInstrument.destroy() + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt index 9d8b4f9..26e4fbb 100644 --- a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt @@ -24,27 +24,55 @@ } } + override var volume: Float = 1.0f + set(value) { + field = value + for (internalInstrument in internalInstruments) { + internalInstrument.volume = volume + } + } + + override var muted: Boolean = false + set(value) { + field = value + for (instrument in internalInstruments) { + instrument.muted = value + } + } + override fun startNote(note: Note) { for ((index, instrumentPlaying) in playing.withIndex()) { if (!instrumentPlaying) { - internalInstruments[index].startNote(note.frequency) + internalInstruments[index].startNote(note) playing[index] = true return } + if (internalInstruments[index].note == note) { + return + } } throw IllegalStateException("cannot start another note with the current amount of oscillators") } - override fun changeActive(newActive: Boolean) { - for (instrument in internalInstruments) { - instrument.active = newActive - } - } - override fun stop() { for ((i, instrument) in internalInstruments.withIndex()) { instrument.endNote() playing[i] = false } } + + override fun stopNote(note: Note) { + for ((i, instrument) in internalInstruments.withIndex()) { + if (instrument.note == note) { + instrument.endNote() + playing[i] = false + } + } + } + + override fun destroy() { + for (instrument in internalInstruments) { + instrument.destroy() + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/Song.kt b/app/src/main/java/com/lukas/music/song/Song.kt index a3123f2..fc5421b 100644 --- a/app/src/main/java/com/lukas/music/song/Song.kt +++ b/app/src/main/java/com/lukas/music/song/Song.kt @@ -16,10 +16,33 @@ import com.lukas.music.util.Cycle class Song( - var root: Note, + root: Note, val beats: Int ) : Cycle(beats) { val chordProgression = ChordProgression() + var soloInstrument: Instrument? = null + set(value) { + field = value + value?.let { + for (instrument in Instrument.instruments) { + if (instrument != value) { + instrument.stop() + } + } + } + } + + var root: Note = root + set(value) { + field = value + stopAllInstruments() + } + + private fun stopAllInstruments() { + for (instrument in Instrument.instruments) { + instrument.stop() + } + } init { for (i in 0 until beats) { @@ -27,6 +50,7 @@ } wraparoundListeners += { chordProgression.step() + stopAllInstruments() } } @@ -34,8 +58,12 @@ super.step() val chord = chordProgression.currentItem?.currentItem ?: return index val chordNotes = chord.getNotes(root) - for (voice in Instrument.voice) { - voice.step(root, chordNotes) + soloInstrument?.let { + it.voice.step(root, chordNotes, index) + } ?: run { + for (instrument in Instrument.instruments) { + instrument.voice.step(root, chordNotes, index) + } } return index } diff --git a/app/src/main/java/com/lukas/music/song/chords/Chord.kt b/app/src/main/java/com/lukas/music/song/chords/Chord.kt index db45fdd..4400dae 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Chord.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Chord.kt @@ -19,6 +19,12 @@ interval = Interval(value) } var interval = Interval(note) + set(value) { + field = value + if (note != value.distance) { + note = value.distance + } + } fun getNotes(root: Note): Array { return Array(chordType.notes.size) { root + note + chordType.notes[it] } diff --git a/app/src/main/java/com/lukas/music/song/chords/Interval.kt b/app/src/main/java/com/lukas/music/song/chords/Interval.kt index 2726787..d112805 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Interval.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Interval.kt @@ -10,7 +10,7 @@ package com.lukas.music.song.chords -class Interval(private val distance: Int) { +class Interval(val distance: Int) { val name: IntervalName = when (distance) { 0 -> IntervalName.UNISON 1, 2 -> IntervalName.SECOND @@ -32,7 +32,7 @@ return name.toString() } - enum class IntervalName(val distance: Int, val romanVersion: String) { + enum class IntervalName(private val distance: Int, val romanVersion: String) { UNISON(0, "I"), SECOND(1, "II"), THIRD(3, "III"), diff --git a/app/src/main/java/com/lukas/music/song/note/Note.kt b/app/src/main/java/com/lukas/music/song/note/Note.kt index d3a588d..17bfb2b 100644 --- a/app/src/main/java/com/lukas/music/song/note/Note.kt +++ b/app/src/main/java/com/lukas/music/song/note/Note.kt @@ -28,6 +28,22 @@ return this + (-other) } + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Note + + if (id != other.id) return false + + return true + } + + override fun hashCode(): Int { + return id + } + + companion object { val NOTES = Array(128) { Note(it) } diff --git a/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt b/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt index 3990554..4706068 100644 --- a/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt +++ b/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt @@ -14,9 +14,16 @@ import com.lukas.music.song.note.Note class BassVoice(instrument: Instrument) : Voice(instrument) { - override val steps = listOf(1, 3) + override var noteActive: Array> = arrayOf( + arrayOf(true), + arrayOf(false), + arrayOf(true), + arrayOf(false) + ) - override fun step(root: Note, chord: Array) { - instrument.startNote(chord[0] - 24) + override val noteCount: Int = 1 + + override fun getNotes(root: Note, chordNotes: Array): Array { + return arrayOf(chordNotes[0] - 24) } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/voice/ChordVoice.kt b/app/src/main/java/com/lukas/music/song/voice/ChordVoice.kt index fc5362d..ab7117f 100644 --- a/app/src/main/java/com/lukas/music/song/voice/ChordVoice.kt +++ b/app/src/main/java/com/lukas/music/song/voice/ChordVoice.kt @@ -14,12 +14,15 @@ import com.lukas.music.song.note.Note class ChordVoice(instrument: Instrument) : Voice(instrument) { - override val steps: List = listOf(2, 4) + override var noteActive: Array> = arrayOf( + Array(3) { false }, + Array(3) { true }, + Array(3) { false }, + Array(3) { true }, + ) + override val noteCount: Int = 3 - override fun step(root: Note, chord: Array) { - instrument.stop() - for (note in chord) { - instrument.startNote(note) - } + override fun getNotes(root: Note, chordNotes: Array): Array { + return chordNotes } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/voice/Voice.kt b/app/src/main/java/com/lukas/music/song/voice/Voice.kt index f7fca94..4f56c2a 100644 --- a/app/src/main/java/com/lukas/music/song/voice/Voice.kt +++ b/app/src/main/java/com/lukas/music/song/voice/Voice.kt @@ -12,9 +12,36 @@ import com.lukas.music.instruments.Instrument import com.lukas.music.song.note.Note +import kotlin.reflect.KClass abstract class Voice(val instrument: Instrument) { - abstract val steps: List + abstract var noteActive: Array> + abstract val noteCount: Int - abstract fun step(root: Note, chord: Array) + abstract fun getNotes(root: Note, chordNotes: Array): Array + + fun step(root: Note, chordNotes: Array, beat: Int) { + if (instrument.muted) { + return + } + val activeNotes = noteActive[beat] + val notes = getNotes(root, chordNotes) + for ((index, active) in activeNotes.withIndex()) { + val note = notes[index] + if (!active) { + instrument.stopNote(note) + continue + } + instrument.startNote(note) + } + } + + companion object { + val DEFAULT_VOICES = listOf>( + BassVoice::class, + ChordVoice::class, + ) + + val DEFAULT_VOICE_NAMES = listOf("Bass", "Chord") + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/adapters/InstrumentAdapter.kt b/app/src/main/java/com/lukas/music/ui/adapters/InstrumentAdapter.kt index 8961831..d51f62e 100644 --- a/app/src/main/java/com/lukas/music/ui/adapters/InstrumentAdapter.kt +++ b/app/src/main/java/com/lukas/music/ui/adapters/InstrumentAdapter.kt @@ -11,54 +11,26 @@ package com.lukas.music.ui.adapters import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup -import android.widget.AdapterView -import android.widget.ArrayAdapter import androidx.recyclerview.widget.RecyclerView import com.lukas.music.databinding.FragmentInstrumentBinding import com.lukas.music.instruments.Instrument -import com.lukas.music.instruments.Waveform +import com.lukas.music.ui.fragments.InstrumentListFragment -class InstrumentAdapter : RecyclerView.Adapter() { - class InstrumentViewHolder(val binding: FragmentInstrumentBinding) : - RecyclerView.ViewHolder(binding.root), AdapterView.OnItemSelectedListener { - lateinit var instrument: Instrument - - init { - val adapter = ArrayAdapter( - binding.root.context, - android.R.layout.simple_spinner_dropdown_item, Waveform.VALUES - ) - binding.waveformSelection.adapter = adapter - binding.waveformSelection.onItemSelectedListener = this - } - - override fun onItemSelected( - adapterView: AdapterView<*>?, - view: View?, - position: Int, - id: Long - ) { - instrument.waveform = Waveform.VALUES[position] - } - - override fun onNothingSelected(adapterView: AdapterView<*>?) { - } - } +class InstrumentAdapter(val parent: InstrumentListFragment) : + RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): InstrumentViewHolder { val context = parent.context val inflater = LayoutInflater.from(context) val binding = FragmentInstrumentBinding.inflate(inflater, parent, false) - return InstrumentViewHolder(binding) + return InstrumentViewHolder(binding, this.parent.childFragmentManager, this) } override fun onBindViewHolder(holder: InstrumentViewHolder, position: Int) { val instrument = Instrument.instruments[position] holder.instrument = instrument - instrument.applyToView(holder.binding) } override fun getItemCount(): Int { diff --git a/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt b/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt new file mode 100644 index 0000000..3e129bc --- /dev/null +++ b/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2022 Lukas Eisenhauer + * + * This program 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, either version 3 of the License, or(at your option) any later version. + * + * This program 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 this program. If not, see . + */ + +package com.lukas.music.ui.adapters + +import androidx.fragment.app.FragmentManager +import androidx.recyclerview.widget.RecyclerView +import com.lukas.music.R +import com.lukas.music.databinding.FragmentInstrumentBinding +import com.lukas.music.instruments.Instrument +import com.lukas.music.song.Song +import com.lukas.music.ui.fragments.EditInstrumentFragment +import com.lukas.music.util.setupToggle +import com.lukas.music.util.updateToggle + +class InstrumentViewHolder( + val binding: FragmentInstrumentBinding, + private val childFragmentManager: FragmentManager, + private val adapter: InstrumentAdapter +) : + RecyclerView.ViewHolder(binding.root) { + init { + HOLDERS += this + } + + var solo: Boolean = false + set(value) { + if (value) { + for (holder in HOLDERS) { + holder.solo = false + } + Song.currentSong.soloInstrument = instrument + } + field = value + binding.soloButton.updateToggle(this::solo, R.color.blue) + } + + var instrument: Instrument? = null + set(value) { + field = value + value ?: return + binding.instrumentNameText.text = instrument?.name + binding.editInstrumentButton.setOnClickListener { + EditInstrumentFragment(instrument!!, this).showNow(childFragmentManager, "") + } + binding.muteButton.setupToggle(instrument!!::muted, R.color.red) + binding.soloButton.setupToggle(this::solo, R.color.blue) { + if (!it) { + Song.currentSong.soloInstrument = null + } + } + binding.deleteButton.setOnClickListener { + val index = Instrument.instruments.indexOf(instrument) + Instrument.instruments -= instrument!! + adapter.notifyItemRemoved(index) + instrument!!.destroy() + } + } + + companion object { + val HOLDERS = mutableListOf() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/fragments/EditChordFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/EditChordFragment.kt index a311a6c..bd7a3d9 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/EditChordFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/EditChordFragment.kt @@ -10,13 +10,10 @@ package com.lukas.music.ui.fragments -import android.R import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.AdapterView -import android.widget.ArrayAdapter import androidx.fragment.app.DialogFragment import com.lukas.music.databinding.FragmentEditChordBinding import com.lukas.music.song.Scale @@ -24,8 +21,10 @@ import com.lukas.music.song.chords.Chord import com.lukas.music.song.chords.ChordType import com.lukas.music.song.chords.Interval +import com.lukas.music.util.setup -class EditChordFragment(val chord: Chord, val songFragment: SongFragment) : DialogFragment() { +class EditChordFragment(val chord: Chord, private val songFragment: SongFragment) : + DialogFragment() { lateinit var binding: FragmentEditChordBinding override fun onCreateView( @@ -45,24 +44,12 @@ val pitches = if (songFragment.displayChordNames) { Array(Scale.MAJOR.steps.size) { (Song.currentSong.root + Scale.MAJOR.steps[it]).noteName.toString() } } else Interval.IntervalName.NAMES - val pitchAdapter = ArrayAdapter( - binding.root.context, - R.layout.simple_spinner_dropdown_item, pitches - ) - binding.pitchSpinner.adapter = pitchAdapter - binding.pitchSpinner.setSelection(chord.interval.name.ordinal) - binding.pitchSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { - override fun onItemSelected( - parent: AdapterView<*>?, - view: View?, - position: Int, - id: Long - ) { - chord.note = Scale.MAJOR.steps[position] - songFragment.updateChords() + binding.pitchSpinner.setup(pitches, chord.interval.name.ordinal) { + chord.note = Scale.MAJOR.steps[it] + if (binding.typeSpinner.selectedItemPosition == 0) { + chord.chordType = Scale.MAJOR.chordTypes[chord.interval.name.ordinal] } - - override fun onNothingSelected(parent: AdapterView<*>?) {} + songFragment.updateChords() } } @@ -71,32 +58,17 @@ for (chordType in ChordType.VALUES) { values += chordType.toString() } - val modifierAdapter = ArrayAdapter( - binding.root.context, - R.layout.simple_spinner_dropdown_item, values - ) - binding.typeSpinner.adapter = modifierAdapter - binding.typeSpinner.setSelection( - if (chord.chordType == Scale.MAJOR.chordTypes[chord.interval.name.ordinal]) - 0 + binding.typeSpinner.setup( + values, + if (chord.chordType == Scale.MAJOR.chordTypes[chord.interval.name.ordinal]) 0 else chord.chordType.ordinal + 1 - ) - binding.typeSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { - override fun onItemSelected( - parent: AdapterView<*>?, - view: View?, - position: Int, - id: Long - ) { - if (position == 0) { - chord.chordType = Scale.MAJOR.chordTypes[chord.interval.name.ordinal] - } else { - chord.chordType = ChordType.VALUES[position - 1] - } - songFragment.updateChords() + ) { + if (it == 0) { + chord.chordType = Scale.MAJOR.chordTypes[chord.interval.name.ordinal] + } else { + chord.chordType = ChordType.VALUES[it - 1] } - - override fun onNothingSelected(parent: AdapterView<*>?) {} + songFragment.updateChords() } } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/fragments/EditInstrumentFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/EditInstrumentFragment.kt new file mode 100644 index 0000000..0020ae8 --- /dev/null +++ b/app/src/main/java/com/lukas/music/ui/fragments/EditInstrumentFragment.kt @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2022 Lukas Eisenhauer + * + * This program 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, either version 3 of the License, or(at your option) any later version. + * + * This program 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 this program. If not, see . + */ + +package com.lukas.music.ui.fragments + +import android.os.Bundle +import android.text.Editable +import android.text.TextWatcher +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.DialogFragment +import com.lukas.music.EditVoiceFragment +import com.lukas.music.databinding.FragmentEditInstrumentBinding +import com.lukas.music.instruments.Instrument +import com.lukas.music.instruments.Waveform +import com.lukas.music.song.voice.Voice +import com.lukas.music.ui.adapters.InstrumentViewHolder +import com.lukas.music.util.setup +import com.lukas.music.util.smartSetup + +class EditInstrumentFragment( + private val instrument: Instrument, + private val viewHolder: InstrumentViewHolder +) : DialogFragment() { + lateinit var binding: FragmentEditInstrumentBinding + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEditInstrumentBinding.inflate(inflater) + binding.instrumentNameTextBox.text.clear() + binding.instrumentNameTextBox.text.append(instrument.name) + binding.instrumentNameTextBox.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} + + override fun afterTextChanged(s: Editable?) { + instrument.name = binding.instrumentNameTextBox.text.toString() + viewHolder.instrument = viewHolder.instrument + } + }) + binding.waveformSelection.smartSetup(Waveform.VALUES, instrument::waveform) + binding.volumeSeek.setup(0, 100, 30) { + binding.volumeText.text = "volume: $it%" + instrument.volume = it.toFloat() / 100f + } + binding.voiceSelection.setup( + Voice.DEFAULT_VOICE_NAMES, + Voice.DEFAULT_VOICES.indexOf(instrument.voice::class) + ) { + if (instrument.voice::class == Voice.DEFAULT_VOICES[it]) { + return@setup + } + instrument.voice = Voice.DEFAULT_VOICES[it].constructors.first().call(instrument) + } + binding.editVoiceButton.setOnClickListener { + EditVoiceFragment(instrument.voice).showNow(childFragmentManager, "") + } + binding.closeButton.setOnClickListener { + dismiss() + } + return binding.root + } + + override fun onStart() { + super.onStart() + dialog?.window?.setLayout( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + } +} \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 97f8bf5..3d886c4 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,6 +11,8 @@ + + diff --git a/app/build.gradle b/app/build.gradle index 35173c4..97ec2a0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -56,12 +56,12 @@ } dependencies { - implementation 'androidx.core:core-ktx:1.7.0' implementation 'androidx.appcompat:appcompat:1.3.0' implementation 'com.google.android.material:material:1.6.1' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'androidx.gridlayout:gridlayout:1.0.0' + implementation 'org.jetbrains.kotlin:kotlin-reflect:1.7.20-Beta' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index dc80bd1..8e98b44 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -15,6 +15,9 @@ } AudioHost *thiz = static_cast(userData); for (auto const &instrument: *thiz->instruments) { + if (!instrument) { + continue; + } instrument->render(buffer, sampleCount); } for (uint32_t i = 0; i < sampleCount; i++) { diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 6ebd2b0..5505a35 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -8,7 +8,7 @@ static AudioHost *audioHost; -template +template void *listGet(_InputIterator iterator, uint32_t n) { for (uint32_t i = 0; i < n; i++) { iterator++; @@ -16,6 +16,18 @@ return *iterator; } +Instrument *getInstrument(uint32_t id) { + return static_cast(listGet(audioHost->instruments->begin(), id)); +} + +template +void listSet(_InputIterator iterator, uint32_t n, void *value) { + for (uint32_t i = 0; i < n; i++) { + iterator++; + } + *iterator = static_cast(value); +} + extern "C" { JNIEXPORT void JNICALL @@ -23,7 +35,6 @@ audioHost = new AudioHost(); } -extern "C" JNIEXPORT jint JNICALL Java_com_lukas_music_instruments_InternalInstrument_createInstrument(JNIEnv *env, jobject thiz) { uint32_t result = audioHost->instruments->size(); @@ -32,43 +43,37 @@ return result; } -extern "C" -JNIEXPORT void JNICALL -Java_com_lukas_music_instruments_InternalInstrument_setInstrumentActive(JNIEnv *env, jobject thiz, - jint id, jboolean active) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->wave->amplitude = active ? 0.3 : 0.0; -} -} -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_startNote(JNIEnv *env, jobject thiz, jint id, jdouble frequency) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->startNote(frequency); + getInstrument(id)->startNote(frequency); } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_endNote(JNIEnv *env, jobject thiz, jint id) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->endNote(); + getInstrument(id)->endNote(); } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_ui_fragments_PlayFragment_setMasterVolume(JNIEnv *env, jobject thiz, jdouble volume) { audioHost->masterVolume = volume; } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setInstrumentWaveform(JNIEnv *env, jobject thiz, jint id, jint waveform) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->setWaveform(static_cast(waveform)); + getInstrument(id)->setWaveform(static_cast(waveform)); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, + jfloat volume) { + getInstrument(id)->wave->amplitude = volume; +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_destroy(JNIEnv *env, jobject thiz, jint id) { + listSet(audioHost->instruments->begin(), id, nullptr); + delete getInstrument(id); +} } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt new file mode 100644 index 0000000..56f21e6 --- /dev/null +++ b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2022 Lukas Eisenhauer + * + * This program 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, either version 3 of the License, or(at your option) any later version. + * + * This program 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 this program. If not, see . + */ + +package com.lukas.music + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TableRow +import androidx.core.view.setMargins +import androidx.fragment.app.DialogFragment +import com.google.android.material.button.MaterialButton +import com.lukas.music.databinding.FragmentEditVoiceBinding +import com.lukas.music.song.Song +import com.lukas.music.song.voice.Voice +import com.lukas.music.util.ArrayProperty +import com.lukas.music.util.setupToggle + +class EditVoiceFragment(private val voice: Voice) : DialogFragment() { + private lateinit var binding: FragmentEditVoiceBinding + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEditVoiceBinding.inflate(inflater) + for (row in 0 until voice.noteCount) { + val rowLayout = TableRow(binding.root.context) + for (column in 0 until Song.currentSong.beats) { + val button = MaterialButton(binding.root.context) + button.layoutParams = buttonLayout + button.setupToggle(ArrayProperty(voice.noteActive[column], row), R.color.blue) + rowLayout.addView(button) + } + binding.noteGrid.addView(rowLayout) + } + binding.noteGrid.isStretchAllColumns = true + binding.closeButton.setOnClickListener { + dismiss() + } + return binding.root + } + + override fun onStart() { + super.onStart() + dialog?.window?.setLayout( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + } + + companion object { + val buttonLayout = TableRow.LayoutParams(0, TableRow.LayoutParams.WRAP_CONTENT) + + init { + buttonLayout.setMargins(5) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Instrument.kt b/app/src/main/java/com/lukas/music/instruments/Instrument.kt index 8ad90c6..92a896c 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -10,42 +10,22 @@ package com.lukas.music.instruments -import com.lukas.music.databinding.FragmentInstrumentBinding import com.lukas.music.song.note.Note import com.lukas.music.song.voice.BassVoice -import com.lukas.music.song.voice.ChordVoice import com.lukas.music.song.voice.Voice -abstract class Instrument(private var name: String) { - private var active = false +abstract class Instrument(var name: String) { + var voice: Voice = BassVoice(this) abstract var waveform: Waveform - - fun applyToView(binding: FragmentInstrumentBinding) { - binding.instrumentNameText.text = name - binding.editInstrumentButton.setOnClickListener { - println("click instrument $name") - } - binding.activeSwitch.setOnCheckedChangeListener { _, newActive -> - active = newActive - changeActive(newActive) - } - binding.activeSwitch.isChecked = active - } + abstract var volume: Float + abstract var muted: Boolean abstract fun startNote(note: Note) abstract fun stop() - abstract fun changeActive(newActive: Boolean) + abstract fun stopNote(note: Note) + abstract fun destroy() companion object { - val instruments = - mutableListOf( - MonoInstrument("Bass"), - PolyInstrument("Chords"), - ) - - val voice = mutableListOf( - BassVoice(instruments[0]), - ChordVoice(instruments[1]), - ) + val instruments = mutableListOf() } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt index dc64bcc..b4c68a4 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -10,33 +10,67 @@ package com.lukas.music.instruments +import com.lukas.music.song.note.Note + class InternalInstrument { private val id = createInstrument() - var active: Boolean = false - set(value) { - field = value - setInstrumentActive(id, value) - } + var note: Note? = null var waveform: Waveform = Waveform.SINE set(value) { field = value setInstrumentWaveform(id, value.id) - // this is to resend the setInstrumentActive for the new waveform in the internal c++ code - active = active + refresh() } - fun startNote(frequency: Double) { - startNote(id, frequency) + var volume: Float = 0.3f + set(value) { + field = value + if (!muted) { + actualVolume = value + } + } + + var muted: Boolean = false + set(value) { + field = value + actualVolume = if (value) 0.0f else volume + } + + private var actualVolume: Float = 1.0f + set(value) { + field = value + setVolume(id, value) + } + + init { + refresh() + } + + private fun refresh() { + // this is to resend the old information to the internal c++ code (when changing the waveform) + muted = muted + volume = volume + } + + fun startNote(note: Note) { + this.note = note + startNote(id, note.frequency) } fun endNote() { + note = null endNote(id) } + fun destroy() { + destroy(id) + } + private external fun createInstrument(): Int - private external fun setInstrumentActive(id: Int, isActive: Boolean) private external fun setInstrumentWaveform(id: Int, waveform: Int) private external fun startNote(id: Int, frequency: Double) private external fun endNote(id: Int) + private external fun setVolume(id: Int, volume: Float) + private external fun destroy(id: Int) } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt index e7b7464..2b43524 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -21,15 +21,36 @@ internalInstrument.waveform = value } - override fun startNote(note: Note) { - internalInstrument.startNote(note.frequency) - } + override var volume: Float = 1.0f + set(value) { + field = value + internalInstrument.volume = volume + } - override fun changeActive(newActive: Boolean) { - internalInstrument.active = newActive + override var muted: Boolean = false + set(value) { + field = value + internalInstrument.muted = value + } + + override fun startNote(note: Note) { + if (note == internalInstrument.note) { + return + } + internalInstrument.startNote(note) } override fun stop() { internalInstrument.endNote() } + + override fun stopNote(note: Note) { + if (note == internalInstrument.note) { + stop() + } + } + + override fun destroy() { + internalInstrument.destroy() + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt index 9d8b4f9..26e4fbb 100644 --- a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt @@ -24,27 +24,55 @@ } } + override var volume: Float = 1.0f + set(value) { + field = value + for (internalInstrument in internalInstruments) { + internalInstrument.volume = volume + } + } + + override var muted: Boolean = false + set(value) { + field = value + for (instrument in internalInstruments) { + instrument.muted = value + } + } + override fun startNote(note: Note) { for ((index, instrumentPlaying) in playing.withIndex()) { if (!instrumentPlaying) { - internalInstruments[index].startNote(note.frequency) + internalInstruments[index].startNote(note) playing[index] = true return } + if (internalInstruments[index].note == note) { + return + } } throw IllegalStateException("cannot start another note with the current amount of oscillators") } - override fun changeActive(newActive: Boolean) { - for (instrument in internalInstruments) { - instrument.active = newActive - } - } - override fun stop() { for ((i, instrument) in internalInstruments.withIndex()) { instrument.endNote() playing[i] = false } } + + override fun stopNote(note: Note) { + for ((i, instrument) in internalInstruments.withIndex()) { + if (instrument.note == note) { + instrument.endNote() + playing[i] = false + } + } + } + + override fun destroy() { + for (instrument in internalInstruments) { + instrument.destroy() + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/Song.kt b/app/src/main/java/com/lukas/music/song/Song.kt index a3123f2..fc5421b 100644 --- a/app/src/main/java/com/lukas/music/song/Song.kt +++ b/app/src/main/java/com/lukas/music/song/Song.kt @@ -16,10 +16,33 @@ import com.lukas.music.util.Cycle class Song( - var root: Note, + root: Note, val beats: Int ) : Cycle(beats) { val chordProgression = ChordProgression() + var soloInstrument: Instrument? = null + set(value) { + field = value + value?.let { + for (instrument in Instrument.instruments) { + if (instrument != value) { + instrument.stop() + } + } + } + } + + var root: Note = root + set(value) { + field = value + stopAllInstruments() + } + + private fun stopAllInstruments() { + for (instrument in Instrument.instruments) { + instrument.stop() + } + } init { for (i in 0 until beats) { @@ -27,6 +50,7 @@ } wraparoundListeners += { chordProgression.step() + stopAllInstruments() } } @@ -34,8 +58,12 @@ super.step() val chord = chordProgression.currentItem?.currentItem ?: return index val chordNotes = chord.getNotes(root) - for (voice in Instrument.voice) { - voice.step(root, chordNotes) + soloInstrument?.let { + it.voice.step(root, chordNotes, index) + } ?: run { + for (instrument in Instrument.instruments) { + instrument.voice.step(root, chordNotes, index) + } } return index } diff --git a/app/src/main/java/com/lukas/music/song/chords/Chord.kt b/app/src/main/java/com/lukas/music/song/chords/Chord.kt index db45fdd..4400dae 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Chord.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Chord.kt @@ -19,6 +19,12 @@ interval = Interval(value) } var interval = Interval(note) + set(value) { + field = value + if (note != value.distance) { + note = value.distance + } + } fun getNotes(root: Note): Array { return Array(chordType.notes.size) { root + note + chordType.notes[it] } diff --git a/app/src/main/java/com/lukas/music/song/chords/Interval.kt b/app/src/main/java/com/lukas/music/song/chords/Interval.kt index 2726787..d112805 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Interval.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Interval.kt @@ -10,7 +10,7 @@ package com.lukas.music.song.chords -class Interval(private val distance: Int) { +class Interval(val distance: Int) { val name: IntervalName = when (distance) { 0 -> IntervalName.UNISON 1, 2 -> IntervalName.SECOND @@ -32,7 +32,7 @@ return name.toString() } - enum class IntervalName(val distance: Int, val romanVersion: String) { + enum class IntervalName(private val distance: Int, val romanVersion: String) { UNISON(0, "I"), SECOND(1, "II"), THIRD(3, "III"), diff --git a/app/src/main/java/com/lukas/music/song/note/Note.kt b/app/src/main/java/com/lukas/music/song/note/Note.kt index d3a588d..17bfb2b 100644 --- a/app/src/main/java/com/lukas/music/song/note/Note.kt +++ b/app/src/main/java/com/lukas/music/song/note/Note.kt @@ -28,6 +28,22 @@ return this + (-other) } + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Note + + if (id != other.id) return false + + return true + } + + override fun hashCode(): Int { + return id + } + + companion object { val NOTES = Array(128) { Note(it) } diff --git a/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt b/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt index 3990554..4706068 100644 --- a/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt +++ b/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt @@ -14,9 +14,16 @@ import com.lukas.music.song.note.Note class BassVoice(instrument: Instrument) : Voice(instrument) { - override val steps = listOf(1, 3) + override var noteActive: Array> = arrayOf( + arrayOf(true), + arrayOf(false), + arrayOf(true), + arrayOf(false) + ) - override fun step(root: Note, chord: Array) { - instrument.startNote(chord[0] - 24) + override val noteCount: Int = 1 + + override fun getNotes(root: Note, chordNotes: Array): Array { + return arrayOf(chordNotes[0] - 24) } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/voice/ChordVoice.kt b/app/src/main/java/com/lukas/music/song/voice/ChordVoice.kt index fc5362d..ab7117f 100644 --- a/app/src/main/java/com/lukas/music/song/voice/ChordVoice.kt +++ b/app/src/main/java/com/lukas/music/song/voice/ChordVoice.kt @@ -14,12 +14,15 @@ import com.lukas.music.song.note.Note class ChordVoice(instrument: Instrument) : Voice(instrument) { - override val steps: List = listOf(2, 4) + override var noteActive: Array> = arrayOf( + Array(3) { false }, + Array(3) { true }, + Array(3) { false }, + Array(3) { true }, + ) + override val noteCount: Int = 3 - override fun step(root: Note, chord: Array) { - instrument.stop() - for (note in chord) { - instrument.startNote(note) - } + override fun getNotes(root: Note, chordNotes: Array): Array { + return chordNotes } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/voice/Voice.kt b/app/src/main/java/com/lukas/music/song/voice/Voice.kt index f7fca94..4f56c2a 100644 --- a/app/src/main/java/com/lukas/music/song/voice/Voice.kt +++ b/app/src/main/java/com/lukas/music/song/voice/Voice.kt @@ -12,9 +12,36 @@ import com.lukas.music.instruments.Instrument import com.lukas.music.song.note.Note +import kotlin.reflect.KClass abstract class Voice(val instrument: Instrument) { - abstract val steps: List + abstract var noteActive: Array> + abstract val noteCount: Int - abstract fun step(root: Note, chord: Array) + abstract fun getNotes(root: Note, chordNotes: Array): Array + + fun step(root: Note, chordNotes: Array, beat: Int) { + if (instrument.muted) { + return + } + val activeNotes = noteActive[beat] + val notes = getNotes(root, chordNotes) + for ((index, active) in activeNotes.withIndex()) { + val note = notes[index] + if (!active) { + instrument.stopNote(note) + continue + } + instrument.startNote(note) + } + } + + companion object { + val DEFAULT_VOICES = listOf>( + BassVoice::class, + ChordVoice::class, + ) + + val DEFAULT_VOICE_NAMES = listOf("Bass", "Chord") + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/adapters/InstrumentAdapter.kt b/app/src/main/java/com/lukas/music/ui/adapters/InstrumentAdapter.kt index 8961831..d51f62e 100644 --- a/app/src/main/java/com/lukas/music/ui/adapters/InstrumentAdapter.kt +++ b/app/src/main/java/com/lukas/music/ui/adapters/InstrumentAdapter.kt @@ -11,54 +11,26 @@ package com.lukas.music.ui.adapters import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup -import android.widget.AdapterView -import android.widget.ArrayAdapter import androidx.recyclerview.widget.RecyclerView import com.lukas.music.databinding.FragmentInstrumentBinding import com.lukas.music.instruments.Instrument -import com.lukas.music.instruments.Waveform +import com.lukas.music.ui.fragments.InstrumentListFragment -class InstrumentAdapter : RecyclerView.Adapter() { - class InstrumentViewHolder(val binding: FragmentInstrumentBinding) : - RecyclerView.ViewHolder(binding.root), AdapterView.OnItemSelectedListener { - lateinit var instrument: Instrument - - init { - val adapter = ArrayAdapter( - binding.root.context, - android.R.layout.simple_spinner_dropdown_item, Waveform.VALUES - ) - binding.waveformSelection.adapter = adapter - binding.waveformSelection.onItemSelectedListener = this - } - - override fun onItemSelected( - adapterView: AdapterView<*>?, - view: View?, - position: Int, - id: Long - ) { - instrument.waveform = Waveform.VALUES[position] - } - - override fun onNothingSelected(adapterView: AdapterView<*>?) { - } - } +class InstrumentAdapter(val parent: InstrumentListFragment) : + RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): InstrumentViewHolder { val context = parent.context val inflater = LayoutInflater.from(context) val binding = FragmentInstrumentBinding.inflate(inflater, parent, false) - return InstrumentViewHolder(binding) + return InstrumentViewHolder(binding, this.parent.childFragmentManager, this) } override fun onBindViewHolder(holder: InstrumentViewHolder, position: Int) { val instrument = Instrument.instruments[position] holder.instrument = instrument - instrument.applyToView(holder.binding) } override fun getItemCount(): Int { diff --git a/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt b/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt new file mode 100644 index 0000000..3e129bc --- /dev/null +++ b/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2022 Lukas Eisenhauer + * + * This program 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, either version 3 of the License, or(at your option) any later version. + * + * This program 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 this program. If not, see . + */ + +package com.lukas.music.ui.adapters + +import androidx.fragment.app.FragmentManager +import androidx.recyclerview.widget.RecyclerView +import com.lukas.music.R +import com.lukas.music.databinding.FragmentInstrumentBinding +import com.lukas.music.instruments.Instrument +import com.lukas.music.song.Song +import com.lukas.music.ui.fragments.EditInstrumentFragment +import com.lukas.music.util.setupToggle +import com.lukas.music.util.updateToggle + +class InstrumentViewHolder( + val binding: FragmentInstrumentBinding, + private val childFragmentManager: FragmentManager, + private val adapter: InstrumentAdapter +) : + RecyclerView.ViewHolder(binding.root) { + init { + HOLDERS += this + } + + var solo: Boolean = false + set(value) { + if (value) { + for (holder in HOLDERS) { + holder.solo = false + } + Song.currentSong.soloInstrument = instrument + } + field = value + binding.soloButton.updateToggle(this::solo, R.color.blue) + } + + var instrument: Instrument? = null + set(value) { + field = value + value ?: return + binding.instrumentNameText.text = instrument?.name + binding.editInstrumentButton.setOnClickListener { + EditInstrumentFragment(instrument!!, this).showNow(childFragmentManager, "") + } + binding.muteButton.setupToggle(instrument!!::muted, R.color.red) + binding.soloButton.setupToggle(this::solo, R.color.blue) { + if (!it) { + Song.currentSong.soloInstrument = null + } + } + binding.deleteButton.setOnClickListener { + val index = Instrument.instruments.indexOf(instrument) + Instrument.instruments -= instrument!! + adapter.notifyItemRemoved(index) + instrument!!.destroy() + } + } + + companion object { + val HOLDERS = mutableListOf() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/fragments/EditChordFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/EditChordFragment.kt index a311a6c..bd7a3d9 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/EditChordFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/EditChordFragment.kt @@ -10,13 +10,10 @@ package com.lukas.music.ui.fragments -import android.R import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.AdapterView -import android.widget.ArrayAdapter import androidx.fragment.app.DialogFragment import com.lukas.music.databinding.FragmentEditChordBinding import com.lukas.music.song.Scale @@ -24,8 +21,10 @@ import com.lukas.music.song.chords.Chord import com.lukas.music.song.chords.ChordType import com.lukas.music.song.chords.Interval +import com.lukas.music.util.setup -class EditChordFragment(val chord: Chord, val songFragment: SongFragment) : DialogFragment() { +class EditChordFragment(val chord: Chord, private val songFragment: SongFragment) : + DialogFragment() { lateinit var binding: FragmentEditChordBinding override fun onCreateView( @@ -45,24 +44,12 @@ val pitches = if (songFragment.displayChordNames) { Array(Scale.MAJOR.steps.size) { (Song.currentSong.root + Scale.MAJOR.steps[it]).noteName.toString() } } else Interval.IntervalName.NAMES - val pitchAdapter = ArrayAdapter( - binding.root.context, - R.layout.simple_spinner_dropdown_item, pitches - ) - binding.pitchSpinner.adapter = pitchAdapter - binding.pitchSpinner.setSelection(chord.interval.name.ordinal) - binding.pitchSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { - override fun onItemSelected( - parent: AdapterView<*>?, - view: View?, - position: Int, - id: Long - ) { - chord.note = Scale.MAJOR.steps[position] - songFragment.updateChords() + binding.pitchSpinner.setup(pitches, chord.interval.name.ordinal) { + chord.note = Scale.MAJOR.steps[it] + if (binding.typeSpinner.selectedItemPosition == 0) { + chord.chordType = Scale.MAJOR.chordTypes[chord.interval.name.ordinal] } - - override fun onNothingSelected(parent: AdapterView<*>?) {} + songFragment.updateChords() } } @@ -71,32 +58,17 @@ for (chordType in ChordType.VALUES) { values += chordType.toString() } - val modifierAdapter = ArrayAdapter( - binding.root.context, - R.layout.simple_spinner_dropdown_item, values - ) - binding.typeSpinner.adapter = modifierAdapter - binding.typeSpinner.setSelection( - if (chord.chordType == Scale.MAJOR.chordTypes[chord.interval.name.ordinal]) - 0 + binding.typeSpinner.setup( + values, + if (chord.chordType == Scale.MAJOR.chordTypes[chord.interval.name.ordinal]) 0 else chord.chordType.ordinal + 1 - ) - binding.typeSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { - override fun onItemSelected( - parent: AdapterView<*>?, - view: View?, - position: Int, - id: Long - ) { - if (position == 0) { - chord.chordType = Scale.MAJOR.chordTypes[chord.interval.name.ordinal] - } else { - chord.chordType = ChordType.VALUES[position - 1] - } - songFragment.updateChords() + ) { + if (it == 0) { + chord.chordType = Scale.MAJOR.chordTypes[chord.interval.name.ordinal] + } else { + chord.chordType = ChordType.VALUES[it - 1] } - - override fun onNothingSelected(parent: AdapterView<*>?) {} + songFragment.updateChords() } } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/fragments/EditInstrumentFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/EditInstrumentFragment.kt new file mode 100644 index 0000000..0020ae8 --- /dev/null +++ b/app/src/main/java/com/lukas/music/ui/fragments/EditInstrumentFragment.kt @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2022 Lukas Eisenhauer + * + * This program 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, either version 3 of the License, or(at your option) any later version. + * + * This program 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 this program. If not, see . + */ + +package com.lukas.music.ui.fragments + +import android.os.Bundle +import android.text.Editable +import android.text.TextWatcher +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.DialogFragment +import com.lukas.music.EditVoiceFragment +import com.lukas.music.databinding.FragmentEditInstrumentBinding +import com.lukas.music.instruments.Instrument +import com.lukas.music.instruments.Waveform +import com.lukas.music.song.voice.Voice +import com.lukas.music.ui.adapters.InstrumentViewHolder +import com.lukas.music.util.setup +import com.lukas.music.util.smartSetup + +class EditInstrumentFragment( + private val instrument: Instrument, + private val viewHolder: InstrumentViewHolder +) : DialogFragment() { + lateinit var binding: FragmentEditInstrumentBinding + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEditInstrumentBinding.inflate(inflater) + binding.instrumentNameTextBox.text.clear() + binding.instrumentNameTextBox.text.append(instrument.name) + binding.instrumentNameTextBox.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} + + override fun afterTextChanged(s: Editable?) { + instrument.name = binding.instrumentNameTextBox.text.toString() + viewHolder.instrument = viewHolder.instrument + } + }) + binding.waveformSelection.smartSetup(Waveform.VALUES, instrument::waveform) + binding.volumeSeek.setup(0, 100, 30) { + binding.volumeText.text = "volume: $it%" + instrument.volume = it.toFloat() / 100f + } + binding.voiceSelection.setup( + Voice.DEFAULT_VOICE_NAMES, + Voice.DEFAULT_VOICES.indexOf(instrument.voice::class) + ) { + if (instrument.voice::class == Voice.DEFAULT_VOICES[it]) { + return@setup + } + instrument.voice = Voice.DEFAULT_VOICES[it].constructors.first().call(instrument) + } + binding.editVoiceButton.setOnClickListener { + EditVoiceFragment(instrument.voice).showNow(childFragmentManager, "") + } + binding.closeButton.setOnClickListener { + dismiss() + } + return binding.root + } + + override fun onStart() { + super.onStart() + dialog?.window?.setLayout( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/fragments/InstrumentListFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/InstrumentListFragment.kt index 3f1a910..1d35c10 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/InstrumentListFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/InstrumentListFragment.kt @@ -10,13 +10,19 @@ package com.lukas.music.ui.fragments +import android.app.AlertDialog import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment +import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView import com.lukas.music.databinding.FragmentInstrumentListBinding +import com.lukas.music.instruments.Instrument +import com.lukas.music.instruments.MonoInstrument +import com.lukas.music.instruments.PolyInstrument import com.lukas.music.ui.adapters.InstrumentAdapter class InstrumentListFragment : Fragment() { @@ -27,8 +33,53 @@ savedInstanceState: Bundle? ): View? { binding = FragmentInstrumentListBinding.inflate(inflater) - binding.recyclerView.adapter = InstrumentAdapter() + binding.recyclerView.adapter = InstrumentAdapter(this) binding.recyclerView.layoutManager = LinearLayoutManager(context) + val callback = object : ItemTouchHelper.SimpleCallback( + ItemTouchHelper.UP or ItemTouchHelper.DOWN, + 0 + ) { + override fun onMove( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder + ): Boolean { + val adapter = recyclerView.adapter as InstrumentAdapter + val startPosition = viewHolder.adapterPosition + val endPosition = target.adapterPosition + val instrument = Instrument.instruments[startPosition] + Instrument.instruments.removeAt(startPosition) + if (endPosition < startPosition) { + Instrument.instruments.add(endPosition + 1, instrument) + } else { + Instrument.instruments.add(endPosition - 1, instrument) + } + adapter.notifyItemMoved(startPosition, endPosition) + return true + } + + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {} + } + val helper = ItemTouchHelper(callback) + helper.attachToRecyclerView(binding.recyclerView) + + val builder = AlertDialog.Builder(binding.root.context) + builder.setTitle("Instrument type:") + .setItems( + arrayOf("mono", "poly") + ) { _, index -> + when (index) { + 0 -> Instrument.instruments += MonoInstrument("New mono Instrument") + 1 -> Instrument.instruments += PolyInstrument("New poly Instrument") + } + (binding.recyclerView.adapter as RecyclerView.Adapter).notifyItemInserted( + Instrument.instruments.size - 1 + ) + } + builder.create() + binding.addInstrumentButton.setOnClickListener { + builder.show() + } return binding.root } } \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 97f8bf5..3d886c4 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,6 +11,8 @@ + + diff --git a/app/build.gradle b/app/build.gradle index 35173c4..97ec2a0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -56,12 +56,12 @@ } dependencies { - implementation 'androidx.core:core-ktx:1.7.0' implementation 'androidx.appcompat:appcompat:1.3.0' implementation 'com.google.android.material:material:1.6.1' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'androidx.gridlayout:gridlayout:1.0.0' + implementation 'org.jetbrains.kotlin:kotlin-reflect:1.7.20-Beta' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index dc80bd1..8e98b44 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -15,6 +15,9 @@ } AudioHost *thiz = static_cast(userData); for (auto const &instrument: *thiz->instruments) { + if (!instrument) { + continue; + } instrument->render(buffer, sampleCount); } for (uint32_t i = 0; i < sampleCount; i++) { diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 6ebd2b0..5505a35 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -8,7 +8,7 @@ static AudioHost *audioHost; -template +template void *listGet(_InputIterator iterator, uint32_t n) { for (uint32_t i = 0; i < n; i++) { iterator++; @@ -16,6 +16,18 @@ return *iterator; } +Instrument *getInstrument(uint32_t id) { + return static_cast(listGet(audioHost->instruments->begin(), id)); +} + +template +void listSet(_InputIterator iterator, uint32_t n, void *value) { + for (uint32_t i = 0; i < n; i++) { + iterator++; + } + *iterator = static_cast(value); +} + extern "C" { JNIEXPORT void JNICALL @@ -23,7 +35,6 @@ audioHost = new AudioHost(); } -extern "C" JNIEXPORT jint JNICALL Java_com_lukas_music_instruments_InternalInstrument_createInstrument(JNIEnv *env, jobject thiz) { uint32_t result = audioHost->instruments->size(); @@ -32,43 +43,37 @@ return result; } -extern "C" -JNIEXPORT void JNICALL -Java_com_lukas_music_instruments_InternalInstrument_setInstrumentActive(JNIEnv *env, jobject thiz, - jint id, jboolean active) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->wave->amplitude = active ? 0.3 : 0.0; -} -} -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_startNote(JNIEnv *env, jobject thiz, jint id, jdouble frequency) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->startNote(frequency); + getInstrument(id)->startNote(frequency); } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_endNote(JNIEnv *env, jobject thiz, jint id) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->endNote(); + getInstrument(id)->endNote(); } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_ui_fragments_PlayFragment_setMasterVolume(JNIEnv *env, jobject thiz, jdouble volume) { audioHost->masterVolume = volume; } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setInstrumentWaveform(JNIEnv *env, jobject thiz, jint id, jint waveform) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->setWaveform(static_cast(waveform)); + getInstrument(id)->setWaveform(static_cast(waveform)); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, + jfloat volume) { + getInstrument(id)->wave->amplitude = volume; +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_destroy(JNIEnv *env, jobject thiz, jint id) { + listSet(audioHost->instruments->begin(), id, nullptr); + delete getInstrument(id); +} } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt new file mode 100644 index 0000000..56f21e6 --- /dev/null +++ b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2022 Lukas Eisenhauer + * + * This program 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, either version 3 of the License, or(at your option) any later version. + * + * This program 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 this program. If not, see . + */ + +package com.lukas.music + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TableRow +import androidx.core.view.setMargins +import androidx.fragment.app.DialogFragment +import com.google.android.material.button.MaterialButton +import com.lukas.music.databinding.FragmentEditVoiceBinding +import com.lukas.music.song.Song +import com.lukas.music.song.voice.Voice +import com.lukas.music.util.ArrayProperty +import com.lukas.music.util.setupToggle + +class EditVoiceFragment(private val voice: Voice) : DialogFragment() { + private lateinit var binding: FragmentEditVoiceBinding + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEditVoiceBinding.inflate(inflater) + for (row in 0 until voice.noteCount) { + val rowLayout = TableRow(binding.root.context) + for (column in 0 until Song.currentSong.beats) { + val button = MaterialButton(binding.root.context) + button.layoutParams = buttonLayout + button.setupToggle(ArrayProperty(voice.noteActive[column], row), R.color.blue) + rowLayout.addView(button) + } + binding.noteGrid.addView(rowLayout) + } + binding.noteGrid.isStretchAllColumns = true + binding.closeButton.setOnClickListener { + dismiss() + } + return binding.root + } + + override fun onStart() { + super.onStart() + dialog?.window?.setLayout( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + } + + companion object { + val buttonLayout = TableRow.LayoutParams(0, TableRow.LayoutParams.WRAP_CONTENT) + + init { + buttonLayout.setMargins(5) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Instrument.kt b/app/src/main/java/com/lukas/music/instruments/Instrument.kt index 8ad90c6..92a896c 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -10,42 +10,22 @@ package com.lukas.music.instruments -import com.lukas.music.databinding.FragmentInstrumentBinding import com.lukas.music.song.note.Note import com.lukas.music.song.voice.BassVoice -import com.lukas.music.song.voice.ChordVoice import com.lukas.music.song.voice.Voice -abstract class Instrument(private var name: String) { - private var active = false +abstract class Instrument(var name: String) { + var voice: Voice = BassVoice(this) abstract var waveform: Waveform - - fun applyToView(binding: FragmentInstrumentBinding) { - binding.instrumentNameText.text = name - binding.editInstrumentButton.setOnClickListener { - println("click instrument $name") - } - binding.activeSwitch.setOnCheckedChangeListener { _, newActive -> - active = newActive - changeActive(newActive) - } - binding.activeSwitch.isChecked = active - } + abstract var volume: Float + abstract var muted: Boolean abstract fun startNote(note: Note) abstract fun stop() - abstract fun changeActive(newActive: Boolean) + abstract fun stopNote(note: Note) + abstract fun destroy() companion object { - val instruments = - mutableListOf( - MonoInstrument("Bass"), - PolyInstrument("Chords"), - ) - - val voice = mutableListOf( - BassVoice(instruments[0]), - ChordVoice(instruments[1]), - ) + val instruments = mutableListOf() } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt index dc64bcc..b4c68a4 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -10,33 +10,67 @@ package com.lukas.music.instruments +import com.lukas.music.song.note.Note + class InternalInstrument { private val id = createInstrument() - var active: Boolean = false - set(value) { - field = value - setInstrumentActive(id, value) - } + var note: Note? = null var waveform: Waveform = Waveform.SINE set(value) { field = value setInstrumentWaveform(id, value.id) - // this is to resend the setInstrumentActive for the new waveform in the internal c++ code - active = active + refresh() } - fun startNote(frequency: Double) { - startNote(id, frequency) + var volume: Float = 0.3f + set(value) { + field = value + if (!muted) { + actualVolume = value + } + } + + var muted: Boolean = false + set(value) { + field = value + actualVolume = if (value) 0.0f else volume + } + + private var actualVolume: Float = 1.0f + set(value) { + field = value + setVolume(id, value) + } + + init { + refresh() + } + + private fun refresh() { + // this is to resend the old information to the internal c++ code (when changing the waveform) + muted = muted + volume = volume + } + + fun startNote(note: Note) { + this.note = note + startNote(id, note.frequency) } fun endNote() { + note = null endNote(id) } + fun destroy() { + destroy(id) + } + private external fun createInstrument(): Int - private external fun setInstrumentActive(id: Int, isActive: Boolean) private external fun setInstrumentWaveform(id: Int, waveform: Int) private external fun startNote(id: Int, frequency: Double) private external fun endNote(id: Int) + private external fun setVolume(id: Int, volume: Float) + private external fun destroy(id: Int) } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt index e7b7464..2b43524 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -21,15 +21,36 @@ internalInstrument.waveform = value } - override fun startNote(note: Note) { - internalInstrument.startNote(note.frequency) - } + override var volume: Float = 1.0f + set(value) { + field = value + internalInstrument.volume = volume + } - override fun changeActive(newActive: Boolean) { - internalInstrument.active = newActive + override var muted: Boolean = false + set(value) { + field = value + internalInstrument.muted = value + } + + override fun startNote(note: Note) { + if (note == internalInstrument.note) { + return + } + internalInstrument.startNote(note) } override fun stop() { internalInstrument.endNote() } + + override fun stopNote(note: Note) { + if (note == internalInstrument.note) { + stop() + } + } + + override fun destroy() { + internalInstrument.destroy() + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt index 9d8b4f9..26e4fbb 100644 --- a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt @@ -24,27 +24,55 @@ } } + override var volume: Float = 1.0f + set(value) { + field = value + for (internalInstrument in internalInstruments) { + internalInstrument.volume = volume + } + } + + override var muted: Boolean = false + set(value) { + field = value + for (instrument in internalInstruments) { + instrument.muted = value + } + } + override fun startNote(note: Note) { for ((index, instrumentPlaying) in playing.withIndex()) { if (!instrumentPlaying) { - internalInstruments[index].startNote(note.frequency) + internalInstruments[index].startNote(note) playing[index] = true return } + if (internalInstruments[index].note == note) { + return + } } throw IllegalStateException("cannot start another note with the current amount of oscillators") } - override fun changeActive(newActive: Boolean) { - for (instrument in internalInstruments) { - instrument.active = newActive - } - } - override fun stop() { for ((i, instrument) in internalInstruments.withIndex()) { instrument.endNote() playing[i] = false } } + + override fun stopNote(note: Note) { + for ((i, instrument) in internalInstruments.withIndex()) { + if (instrument.note == note) { + instrument.endNote() + playing[i] = false + } + } + } + + override fun destroy() { + for (instrument in internalInstruments) { + instrument.destroy() + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/Song.kt b/app/src/main/java/com/lukas/music/song/Song.kt index a3123f2..fc5421b 100644 --- a/app/src/main/java/com/lukas/music/song/Song.kt +++ b/app/src/main/java/com/lukas/music/song/Song.kt @@ -16,10 +16,33 @@ import com.lukas.music.util.Cycle class Song( - var root: Note, + root: Note, val beats: Int ) : Cycle(beats) { val chordProgression = ChordProgression() + var soloInstrument: Instrument? = null + set(value) { + field = value + value?.let { + for (instrument in Instrument.instruments) { + if (instrument != value) { + instrument.stop() + } + } + } + } + + var root: Note = root + set(value) { + field = value + stopAllInstruments() + } + + private fun stopAllInstruments() { + for (instrument in Instrument.instruments) { + instrument.stop() + } + } init { for (i in 0 until beats) { @@ -27,6 +50,7 @@ } wraparoundListeners += { chordProgression.step() + stopAllInstruments() } } @@ -34,8 +58,12 @@ super.step() val chord = chordProgression.currentItem?.currentItem ?: return index val chordNotes = chord.getNotes(root) - for (voice in Instrument.voice) { - voice.step(root, chordNotes) + soloInstrument?.let { + it.voice.step(root, chordNotes, index) + } ?: run { + for (instrument in Instrument.instruments) { + instrument.voice.step(root, chordNotes, index) + } } return index } diff --git a/app/src/main/java/com/lukas/music/song/chords/Chord.kt b/app/src/main/java/com/lukas/music/song/chords/Chord.kt index db45fdd..4400dae 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Chord.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Chord.kt @@ -19,6 +19,12 @@ interval = Interval(value) } var interval = Interval(note) + set(value) { + field = value + if (note != value.distance) { + note = value.distance + } + } fun getNotes(root: Note): Array { return Array(chordType.notes.size) { root + note + chordType.notes[it] } diff --git a/app/src/main/java/com/lukas/music/song/chords/Interval.kt b/app/src/main/java/com/lukas/music/song/chords/Interval.kt index 2726787..d112805 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Interval.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Interval.kt @@ -10,7 +10,7 @@ package com.lukas.music.song.chords -class Interval(private val distance: Int) { +class Interval(val distance: Int) { val name: IntervalName = when (distance) { 0 -> IntervalName.UNISON 1, 2 -> IntervalName.SECOND @@ -32,7 +32,7 @@ return name.toString() } - enum class IntervalName(val distance: Int, val romanVersion: String) { + enum class IntervalName(private val distance: Int, val romanVersion: String) { UNISON(0, "I"), SECOND(1, "II"), THIRD(3, "III"), diff --git a/app/src/main/java/com/lukas/music/song/note/Note.kt b/app/src/main/java/com/lukas/music/song/note/Note.kt index d3a588d..17bfb2b 100644 --- a/app/src/main/java/com/lukas/music/song/note/Note.kt +++ b/app/src/main/java/com/lukas/music/song/note/Note.kt @@ -28,6 +28,22 @@ return this + (-other) } + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Note + + if (id != other.id) return false + + return true + } + + override fun hashCode(): Int { + return id + } + + companion object { val NOTES = Array(128) { Note(it) } diff --git a/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt b/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt index 3990554..4706068 100644 --- a/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt +++ b/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt @@ -14,9 +14,16 @@ import com.lukas.music.song.note.Note class BassVoice(instrument: Instrument) : Voice(instrument) { - override val steps = listOf(1, 3) + override var noteActive: Array> = arrayOf( + arrayOf(true), + arrayOf(false), + arrayOf(true), + arrayOf(false) + ) - override fun step(root: Note, chord: Array) { - instrument.startNote(chord[0] - 24) + override val noteCount: Int = 1 + + override fun getNotes(root: Note, chordNotes: Array): Array { + return arrayOf(chordNotes[0] - 24) } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/voice/ChordVoice.kt b/app/src/main/java/com/lukas/music/song/voice/ChordVoice.kt index fc5362d..ab7117f 100644 --- a/app/src/main/java/com/lukas/music/song/voice/ChordVoice.kt +++ b/app/src/main/java/com/lukas/music/song/voice/ChordVoice.kt @@ -14,12 +14,15 @@ import com.lukas.music.song.note.Note class ChordVoice(instrument: Instrument) : Voice(instrument) { - override val steps: List = listOf(2, 4) + override var noteActive: Array> = arrayOf( + Array(3) { false }, + Array(3) { true }, + Array(3) { false }, + Array(3) { true }, + ) + override val noteCount: Int = 3 - override fun step(root: Note, chord: Array) { - instrument.stop() - for (note in chord) { - instrument.startNote(note) - } + override fun getNotes(root: Note, chordNotes: Array): Array { + return chordNotes } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/voice/Voice.kt b/app/src/main/java/com/lukas/music/song/voice/Voice.kt index f7fca94..4f56c2a 100644 --- a/app/src/main/java/com/lukas/music/song/voice/Voice.kt +++ b/app/src/main/java/com/lukas/music/song/voice/Voice.kt @@ -12,9 +12,36 @@ import com.lukas.music.instruments.Instrument import com.lukas.music.song.note.Note +import kotlin.reflect.KClass abstract class Voice(val instrument: Instrument) { - abstract val steps: List + abstract var noteActive: Array> + abstract val noteCount: Int - abstract fun step(root: Note, chord: Array) + abstract fun getNotes(root: Note, chordNotes: Array): Array + + fun step(root: Note, chordNotes: Array, beat: Int) { + if (instrument.muted) { + return + } + val activeNotes = noteActive[beat] + val notes = getNotes(root, chordNotes) + for ((index, active) in activeNotes.withIndex()) { + val note = notes[index] + if (!active) { + instrument.stopNote(note) + continue + } + instrument.startNote(note) + } + } + + companion object { + val DEFAULT_VOICES = listOf>( + BassVoice::class, + ChordVoice::class, + ) + + val DEFAULT_VOICE_NAMES = listOf("Bass", "Chord") + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/adapters/InstrumentAdapter.kt b/app/src/main/java/com/lukas/music/ui/adapters/InstrumentAdapter.kt index 8961831..d51f62e 100644 --- a/app/src/main/java/com/lukas/music/ui/adapters/InstrumentAdapter.kt +++ b/app/src/main/java/com/lukas/music/ui/adapters/InstrumentAdapter.kt @@ -11,54 +11,26 @@ package com.lukas.music.ui.adapters import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup -import android.widget.AdapterView -import android.widget.ArrayAdapter import androidx.recyclerview.widget.RecyclerView import com.lukas.music.databinding.FragmentInstrumentBinding import com.lukas.music.instruments.Instrument -import com.lukas.music.instruments.Waveform +import com.lukas.music.ui.fragments.InstrumentListFragment -class InstrumentAdapter : RecyclerView.Adapter() { - class InstrumentViewHolder(val binding: FragmentInstrumentBinding) : - RecyclerView.ViewHolder(binding.root), AdapterView.OnItemSelectedListener { - lateinit var instrument: Instrument - - init { - val adapter = ArrayAdapter( - binding.root.context, - android.R.layout.simple_spinner_dropdown_item, Waveform.VALUES - ) - binding.waveformSelection.adapter = adapter - binding.waveformSelection.onItemSelectedListener = this - } - - override fun onItemSelected( - adapterView: AdapterView<*>?, - view: View?, - position: Int, - id: Long - ) { - instrument.waveform = Waveform.VALUES[position] - } - - override fun onNothingSelected(adapterView: AdapterView<*>?) { - } - } +class InstrumentAdapter(val parent: InstrumentListFragment) : + RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): InstrumentViewHolder { val context = parent.context val inflater = LayoutInflater.from(context) val binding = FragmentInstrumentBinding.inflate(inflater, parent, false) - return InstrumentViewHolder(binding) + return InstrumentViewHolder(binding, this.parent.childFragmentManager, this) } override fun onBindViewHolder(holder: InstrumentViewHolder, position: Int) { val instrument = Instrument.instruments[position] holder.instrument = instrument - instrument.applyToView(holder.binding) } override fun getItemCount(): Int { diff --git a/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt b/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt new file mode 100644 index 0000000..3e129bc --- /dev/null +++ b/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2022 Lukas Eisenhauer + * + * This program 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, either version 3 of the License, or(at your option) any later version. + * + * This program 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 this program. If not, see . + */ + +package com.lukas.music.ui.adapters + +import androidx.fragment.app.FragmentManager +import androidx.recyclerview.widget.RecyclerView +import com.lukas.music.R +import com.lukas.music.databinding.FragmentInstrumentBinding +import com.lukas.music.instruments.Instrument +import com.lukas.music.song.Song +import com.lukas.music.ui.fragments.EditInstrumentFragment +import com.lukas.music.util.setupToggle +import com.lukas.music.util.updateToggle + +class InstrumentViewHolder( + val binding: FragmentInstrumentBinding, + private val childFragmentManager: FragmentManager, + private val adapter: InstrumentAdapter +) : + RecyclerView.ViewHolder(binding.root) { + init { + HOLDERS += this + } + + var solo: Boolean = false + set(value) { + if (value) { + for (holder in HOLDERS) { + holder.solo = false + } + Song.currentSong.soloInstrument = instrument + } + field = value + binding.soloButton.updateToggle(this::solo, R.color.blue) + } + + var instrument: Instrument? = null + set(value) { + field = value + value ?: return + binding.instrumentNameText.text = instrument?.name + binding.editInstrumentButton.setOnClickListener { + EditInstrumentFragment(instrument!!, this).showNow(childFragmentManager, "") + } + binding.muteButton.setupToggle(instrument!!::muted, R.color.red) + binding.soloButton.setupToggle(this::solo, R.color.blue) { + if (!it) { + Song.currentSong.soloInstrument = null + } + } + binding.deleteButton.setOnClickListener { + val index = Instrument.instruments.indexOf(instrument) + Instrument.instruments -= instrument!! + adapter.notifyItemRemoved(index) + instrument!!.destroy() + } + } + + companion object { + val HOLDERS = mutableListOf() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/fragments/EditChordFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/EditChordFragment.kt index a311a6c..bd7a3d9 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/EditChordFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/EditChordFragment.kt @@ -10,13 +10,10 @@ package com.lukas.music.ui.fragments -import android.R import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.AdapterView -import android.widget.ArrayAdapter import androidx.fragment.app.DialogFragment import com.lukas.music.databinding.FragmentEditChordBinding import com.lukas.music.song.Scale @@ -24,8 +21,10 @@ import com.lukas.music.song.chords.Chord import com.lukas.music.song.chords.ChordType import com.lukas.music.song.chords.Interval +import com.lukas.music.util.setup -class EditChordFragment(val chord: Chord, val songFragment: SongFragment) : DialogFragment() { +class EditChordFragment(val chord: Chord, private val songFragment: SongFragment) : + DialogFragment() { lateinit var binding: FragmentEditChordBinding override fun onCreateView( @@ -45,24 +44,12 @@ val pitches = if (songFragment.displayChordNames) { Array(Scale.MAJOR.steps.size) { (Song.currentSong.root + Scale.MAJOR.steps[it]).noteName.toString() } } else Interval.IntervalName.NAMES - val pitchAdapter = ArrayAdapter( - binding.root.context, - R.layout.simple_spinner_dropdown_item, pitches - ) - binding.pitchSpinner.adapter = pitchAdapter - binding.pitchSpinner.setSelection(chord.interval.name.ordinal) - binding.pitchSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { - override fun onItemSelected( - parent: AdapterView<*>?, - view: View?, - position: Int, - id: Long - ) { - chord.note = Scale.MAJOR.steps[position] - songFragment.updateChords() + binding.pitchSpinner.setup(pitches, chord.interval.name.ordinal) { + chord.note = Scale.MAJOR.steps[it] + if (binding.typeSpinner.selectedItemPosition == 0) { + chord.chordType = Scale.MAJOR.chordTypes[chord.interval.name.ordinal] } - - override fun onNothingSelected(parent: AdapterView<*>?) {} + songFragment.updateChords() } } @@ -71,32 +58,17 @@ for (chordType in ChordType.VALUES) { values += chordType.toString() } - val modifierAdapter = ArrayAdapter( - binding.root.context, - R.layout.simple_spinner_dropdown_item, values - ) - binding.typeSpinner.adapter = modifierAdapter - binding.typeSpinner.setSelection( - if (chord.chordType == Scale.MAJOR.chordTypes[chord.interval.name.ordinal]) - 0 + binding.typeSpinner.setup( + values, + if (chord.chordType == Scale.MAJOR.chordTypes[chord.interval.name.ordinal]) 0 else chord.chordType.ordinal + 1 - ) - binding.typeSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { - override fun onItemSelected( - parent: AdapterView<*>?, - view: View?, - position: Int, - id: Long - ) { - if (position == 0) { - chord.chordType = Scale.MAJOR.chordTypes[chord.interval.name.ordinal] - } else { - chord.chordType = ChordType.VALUES[position - 1] - } - songFragment.updateChords() + ) { + if (it == 0) { + chord.chordType = Scale.MAJOR.chordTypes[chord.interval.name.ordinal] + } else { + chord.chordType = ChordType.VALUES[it - 1] } - - override fun onNothingSelected(parent: AdapterView<*>?) {} + songFragment.updateChords() } } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/fragments/EditInstrumentFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/EditInstrumentFragment.kt new file mode 100644 index 0000000..0020ae8 --- /dev/null +++ b/app/src/main/java/com/lukas/music/ui/fragments/EditInstrumentFragment.kt @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2022 Lukas Eisenhauer + * + * This program 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, either version 3 of the License, or(at your option) any later version. + * + * This program 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 this program. If not, see . + */ + +package com.lukas.music.ui.fragments + +import android.os.Bundle +import android.text.Editable +import android.text.TextWatcher +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.DialogFragment +import com.lukas.music.EditVoiceFragment +import com.lukas.music.databinding.FragmentEditInstrumentBinding +import com.lukas.music.instruments.Instrument +import com.lukas.music.instruments.Waveform +import com.lukas.music.song.voice.Voice +import com.lukas.music.ui.adapters.InstrumentViewHolder +import com.lukas.music.util.setup +import com.lukas.music.util.smartSetup + +class EditInstrumentFragment( + private val instrument: Instrument, + private val viewHolder: InstrumentViewHolder +) : DialogFragment() { + lateinit var binding: FragmentEditInstrumentBinding + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEditInstrumentBinding.inflate(inflater) + binding.instrumentNameTextBox.text.clear() + binding.instrumentNameTextBox.text.append(instrument.name) + binding.instrumentNameTextBox.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} + + override fun afterTextChanged(s: Editable?) { + instrument.name = binding.instrumentNameTextBox.text.toString() + viewHolder.instrument = viewHolder.instrument + } + }) + binding.waveformSelection.smartSetup(Waveform.VALUES, instrument::waveform) + binding.volumeSeek.setup(0, 100, 30) { + binding.volumeText.text = "volume: $it%" + instrument.volume = it.toFloat() / 100f + } + binding.voiceSelection.setup( + Voice.DEFAULT_VOICE_NAMES, + Voice.DEFAULT_VOICES.indexOf(instrument.voice::class) + ) { + if (instrument.voice::class == Voice.DEFAULT_VOICES[it]) { + return@setup + } + instrument.voice = Voice.DEFAULT_VOICES[it].constructors.first().call(instrument) + } + binding.editVoiceButton.setOnClickListener { + EditVoiceFragment(instrument.voice).showNow(childFragmentManager, "") + } + binding.closeButton.setOnClickListener { + dismiss() + } + return binding.root + } + + override fun onStart() { + super.onStart() + dialog?.window?.setLayout( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/fragments/InstrumentListFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/InstrumentListFragment.kt index 3f1a910..1d35c10 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/InstrumentListFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/InstrumentListFragment.kt @@ -10,13 +10,19 @@ package com.lukas.music.ui.fragments +import android.app.AlertDialog import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment +import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView import com.lukas.music.databinding.FragmentInstrumentListBinding +import com.lukas.music.instruments.Instrument +import com.lukas.music.instruments.MonoInstrument +import com.lukas.music.instruments.PolyInstrument import com.lukas.music.ui.adapters.InstrumentAdapter class InstrumentListFragment : Fragment() { @@ -27,8 +33,53 @@ savedInstanceState: Bundle? ): View? { binding = FragmentInstrumentListBinding.inflate(inflater) - binding.recyclerView.adapter = InstrumentAdapter() + binding.recyclerView.adapter = InstrumentAdapter(this) binding.recyclerView.layoutManager = LinearLayoutManager(context) + val callback = object : ItemTouchHelper.SimpleCallback( + ItemTouchHelper.UP or ItemTouchHelper.DOWN, + 0 + ) { + override fun onMove( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder + ): Boolean { + val adapter = recyclerView.adapter as InstrumentAdapter + val startPosition = viewHolder.adapterPosition + val endPosition = target.adapterPosition + val instrument = Instrument.instruments[startPosition] + Instrument.instruments.removeAt(startPosition) + if (endPosition < startPosition) { + Instrument.instruments.add(endPosition + 1, instrument) + } else { + Instrument.instruments.add(endPosition - 1, instrument) + } + adapter.notifyItemMoved(startPosition, endPosition) + return true + } + + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {} + } + val helper = ItemTouchHelper(callback) + helper.attachToRecyclerView(binding.recyclerView) + + val builder = AlertDialog.Builder(binding.root.context) + builder.setTitle("Instrument type:") + .setItems( + arrayOf("mono", "poly") + ) { _, index -> + when (index) { + 0 -> Instrument.instruments += MonoInstrument("New mono Instrument") + 1 -> Instrument.instruments += PolyInstrument("New poly Instrument") + } + (binding.recyclerView.adapter as RecyclerView.Adapter).notifyItemInserted( + Instrument.instruments.size - 1 + ) + } + builder.create() + binding.addInstrumentButton.setOnClickListener { + builder.show() + } return binding.root } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt index eaf2c61..0c33019 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt @@ -16,7 +16,10 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.* +import android.widget.RadioButton +import android.widget.RadioGroup +import android.widget.Space +import android.widget.TextView import androidx.cardview.widget.CardView import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment @@ -24,6 +27,7 @@ import com.lukas.music.databinding.FragmentPlayBinding import com.lukas.music.instruments.Rhythm import com.lukas.music.song.Song +import com.lukas.music.util.setup class PlayFragment : Fragment() { private lateinit var binding: FragmentPlayBinding @@ -58,11 +62,11 @@ } Song.currentSong.chordProgression.reverse() } - setupSlider(binding.masterVolumeSlider, 0, 100, 100) { + binding.masterVolumeSlider.setup(0, 100, 100) { setMasterVolume(it.toDouble() / 100.0) binding.masterVolumeText.text = "Master volume: $it%" } - setupSlider(binding.tempoSlider, 50, 150, 90) { + binding.tempoSlider.setup(50, 150, 90) { Rhythm.setTempo(it) binding.tempoText.text = "tempo: ${it}bpm" } @@ -116,33 +120,6 @@ ) } - private fun setupSlider( - slider: SeekBar, - min: Int, - max: Int, - initialProgress: Int, - callback: (Int) -> Unit - ) { - slider.min = min - slider.max = max - slider.setOnSeekBarChangeListener(object : - SeekBar.OnSeekBarChangeListener { - override fun onProgressChanged( - seekBar: SeekBar, - progress: Int, fromUser: Boolean - ) { - callback(progress) - } - - override fun onStartTrackingTouch(seekBar: SeekBar) { - } - - override fun onStopTrackingTouch(seekBar: SeekBar) { - } - }) - slider.progress = initialProgress - } - fun updateChords() { binding.phraseDisplay.removeAllViews() chordDisplays.clear() diff --git a/.idea/misc.xml b/.idea/misc.xml index 97f8bf5..3d886c4 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,6 +11,8 @@ + + diff --git a/app/build.gradle b/app/build.gradle index 35173c4..97ec2a0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -56,12 +56,12 @@ } dependencies { - implementation 'androidx.core:core-ktx:1.7.0' implementation 'androidx.appcompat:appcompat:1.3.0' implementation 'com.google.android.material:material:1.6.1' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'androidx.gridlayout:gridlayout:1.0.0' + implementation 'org.jetbrains.kotlin:kotlin-reflect:1.7.20-Beta' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index dc80bd1..8e98b44 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -15,6 +15,9 @@ } AudioHost *thiz = static_cast(userData); for (auto const &instrument: *thiz->instruments) { + if (!instrument) { + continue; + } instrument->render(buffer, sampleCount); } for (uint32_t i = 0; i < sampleCount; i++) { diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 6ebd2b0..5505a35 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -8,7 +8,7 @@ static AudioHost *audioHost; -template +template void *listGet(_InputIterator iterator, uint32_t n) { for (uint32_t i = 0; i < n; i++) { iterator++; @@ -16,6 +16,18 @@ return *iterator; } +Instrument *getInstrument(uint32_t id) { + return static_cast(listGet(audioHost->instruments->begin(), id)); +} + +template +void listSet(_InputIterator iterator, uint32_t n, void *value) { + for (uint32_t i = 0; i < n; i++) { + iterator++; + } + *iterator = static_cast(value); +} + extern "C" { JNIEXPORT void JNICALL @@ -23,7 +35,6 @@ audioHost = new AudioHost(); } -extern "C" JNIEXPORT jint JNICALL Java_com_lukas_music_instruments_InternalInstrument_createInstrument(JNIEnv *env, jobject thiz) { uint32_t result = audioHost->instruments->size(); @@ -32,43 +43,37 @@ return result; } -extern "C" -JNIEXPORT void JNICALL -Java_com_lukas_music_instruments_InternalInstrument_setInstrumentActive(JNIEnv *env, jobject thiz, - jint id, jboolean active) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->wave->amplitude = active ? 0.3 : 0.0; -} -} -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_startNote(JNIEnv *env, jobject thiz, jint id, jdouble frequency) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->startNote(frequency); + getInstrument(id)->startNote(frequency); } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_endNote(JNIEnv *env, jobject thiz, jint id) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->endNote(); + getInstrument(id)->endNote(); } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_ui_fragments_PlayFragment_setMasterVolume(JNIEnv *env, jobject thiz, jdouble volume) { audioHost->masterVolume = volume; } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setInstrumentWaveform(JNIEnv *env, jobject thiz, jint id, jint waveform) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->setWaveform(static_cast(waveform)); + getInstrument(id)->setWaveform(static_cast(waveform)); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, + jfloat volume) { + getInstrument(id)->wave->amplitude = volume; +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_destroy(JNIEnv *env, jobject thiz, jint id) { + listSet(audioHost->instruments->begin(), id, nullptr); + delete getInstrument(id); +} } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt new file mode 100644 index 0000000..56f21e6 --- /dev/null +++ b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2022 Lukas Eisenhauer + * + * This program 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, either version 3 of the License, or(at your option) any later version. + * + * This program 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 this program. If not, see . + */ + +package com.lukas.music + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TableRow +import androidx.core.view.setMargins +import androidx.fragment.app.DialogFragment +import com.google.android.material.button.MaterialButton +import com.lukas.music.databinding.FragmentEditVoiceBinding +import com.lukas.music.song.Song +import com.lukas.music.song.voice.Voice +import com.lukas.music.util.ArrayProperty +import com.lukas.music.util.setupToggle + +class EditVoiceFragment(private val voice: Voice) : DialogFragment() { + private lateinit var binding: FragmentEditVoiceBinding + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEditVoiceBinding.inflate(inflater) + for (row in 0 until voice.noteCount) { + val rowLayout = TableRow(binding.root.context) + for (column in 0 until Song.currentSong.beats) { + val button = MaterialButton(binding.root.context) + button.layoutParams = buttonLayout + button.setupToggle(ArrayProperty(voice.noteActive[column], row), R.color.blue) + rowLayout.addView(button) + } + binding.noteGrid.addView(rowLayout) + } + binding.noteGrid.isStretchAllColumns = true + binding.closeButton.setOnClickListener { + dismiss() + } + return binding.root + } + + override fun onStart() { + super.onStart() + dialog?.window?.setLayout( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + } + + companion object { + val buttonLayout = TableRow.LayoutParams(0, TableRow.LayoutParams.WRAP_CONTENT) + + init { + buttonLayout.setMargins(5) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Instrument.kt b/app/src/main/java/com/lukas/music/instruments/Instrument.kt index 8ad90c6..92a896c 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -10,42 +10,22 @@ package com.lukas.music.instruments -import com.lukas.music.databinding.FragmentInstrumentBinding import com.lukas.music.song.note.Note import com.lukas.music.song.voice.BassVoice -import com.lukas.music.song.voice.ChordVoice import com.lukas.music.song.voice.Voice -abstract class Instrument(private var name: String) { - private var active = false +abstract class Instrument(var name: String) { + var voice: Voice = BassVoice(this) abstract var waveform: Waveform - - fun applyToView(binding: FragmentInstrumentBinding) { - binding.instrumentNameText.text = name - binding.editInstrumentButton.setOnClickListener { - println("click instrument $name") - } - binding.activeSwitch.setOnCheckedChangeListener { _, newActive -> - active = newActive - changeActive(newActive) - } - binding.activeSwitch.isChecked = active - } + abstract var volume: Float + abstract var muted: Boolean abstract fun startNote(note: Note) abstract fun stop() - abstract fun changeActive(newActive: Boolean) + abstract fun stopNote(note: Note) + abstract fun destroy() companion object { - val instruments = - mutableListOf( - MonoInstrument("Bass"), - PolyInstrument("Chords"), - ) - - val voice = mutableListOf( - BassVoice(instruments[0]), - ChordVoice(instruments[1]), - ) + val instruments = mutableListOf() } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt index dc64bcc..b4c68a4 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -10,33 +10,67 @@ package com.lukas.music.instruments +import com.lukas.music.song.note.Note + class InternalInstrument { private val id = createInstrument() - var active: Boolean = false - set(value) { - field = value - setInstrumentActive(id, value) - } + var note: Note? = null var waveform: Waveform = Waveform.SINE set(value) { field = value setInstrumentWaveform(id, value.id) - // this is to resend the setInstrumentActive for the new waveform in the internal c++ code - active = active + refresh() } - fun startNote(frequency: Double) { - startNote(id, frequency) + var volume: Float = 0.3f + set(value) { + field = value + if (!muted) { + actualVolume = value + } + } + + var muted: Boolean = false + set(value) { + field = value + actualVolume = if (value) 0.0f else volume + } + + private var actualVolume: Float = 1.0f + set(value) { + field = value + setVolume(id, value) + } + + init { + refresh() + } + + private fun refresh() { + // this is to resend the old information to the internal c++ code (when changing the waveform) + muted = muted + volume = volume + } + + fun startNote(note: Note) { + this.note = note + startNote(id, note.frequency) } fun endNote() { + note = null endNote(id) } + fun destroy() { + destroy(id) + } + private external fun createInstrument(): Int - private external fun setInstrumentActive(id: Int, isActive: Boolean) private external fun setInstrumentWaveform(id: Int, waveform: Int) private external fun startNote(id: Int, frequency: Double) private external fun endNote(id: Int) + private external fun setVolume(id: Int, volume: Float) + private external fun destroy(id: Int) } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt index e7b7464..2b43524 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -21,15 +21,36 @@ internalInstrument.waveform = value } - override fun startNote(note: Note) { - internalInstrument.startNote(note.frequency) - } + override var volume: Float = 1.0f + set(value) { + field = value + internalInstrument.volume = volume + } - override fun changeActive(newActive: Boolean) { - internalInstrument.active = newActive + override var muted: Boolean = false + set(value) { + field = value + internalInstrument.muted = value + } + + override fun startNote(note: Note) { + if (note == internalInstrument.note) { + return + } + internalInstrument.startNote(note) } override fun stop() { internalInstrument.endNote() } + + override fun stopNote(note: Note) { + if (note == internalInstrument.note) { + stop() + } + } + + override fun destroy() { + internalInstrument.destroy() + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt index 9d8b4f9..26e4fbb 100644 --- a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt @@ -24,27 +24,55 @@ } } + override var volume: Float = 1.0f + set(value) { + field = value + for (internalInstrument in internalInstruments) { + internalInstrument.volume = volume + } + } + + override var muted: Boolean = false + set(value) { + field = value + for (instrument in internalInstruments) { + instrument.muted = value + } + } + override fun startNote(note: Note) { for ((index, instrumentPlaying) in playing.withIndex()) { if (!instrumentPlaying) { - internalInstruments[index].startNote(note.frequency) + internalInstruments[index].startNote(note) playing[index] = true return } + if (internalInstruments[index].note == note) { + return + } } throw IllegalStateException("cannot start another note with the current amount of oscillators") } - override fun changeActive(newActive: Boolean) { - for (instrument in internalInstruments) { - instrument.active = newActive - } - } - override fun stop() { for ((i, instrument) in internalInstruments.withIndex()) { instrument.endNote() playing[i] = false } } + + override fun stopNote(note: Note) { + for ((i, instrument) in internalInstruments.withIndex()) { + if (instrument.note == note) { + instrument.endNote() + playing[i] = false + } + } + } + + override fun destroy() { + for (instrument in internalInstruments) { + instrument.destroy() + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/Song.kt b/app/src/main/java/com/lukas/music/song/Song.kt index a3123f2..fc5421b 100644 --- a/app/src/main/java/com/lukas/music/song/Song.kt +++ b/app/src/main/java/com/lukas/music/song/Song.kt @@ -16,10 +16,33 @@ import com.lukas.music.util.Cycle class Song( - var root: Note, + root: Note, val beats: Int ) : Cycle(beats) { val chordProgression = ChordProgression() + var soloInstrument: Instrument? = null + set(value) { + field = value + value?.let { + for (instrument in Instrument.instruments) { + if (instrument != value) { + instrument.stop() + } + } + } + } + + var root: Note = root + set(value) { + field = value + stopAllInstruments() + } + + private fun stopAllInstruments() { + for (instrument in Instrument.instruments) { + instrument.stop() + } + } init { for (i in 0 until beats) { @@ -27,6 +50,7 @@ } wraparoundListeners += { chordProgression.step() + stopAllInstruments() } } @@ -34,8 +58,12 @@ super.step() val chord = chordProgression.currentItem?.currentItem ?: return index val chordNotes = chord.getNotes(root) - for (voice in Instrument.voice) { - voice.step(root, chordNotes) + soloInstrument?.let { + it.voice.step(root, chordNotes, index) + } ?: run { + for (instrument in Instrument.instruments) { + instrument.voice.step(root, chordNotes, index) + } } return index } diff --git a/app/src/main/java/com/lukas/music/song/chords/Chord.kt b/app/src/main/java/com/lukas/music/song/chords/Chord.kt index db45fdd..4400dae 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Chord.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Chord.kt @@ -19,6 +19,12 @@ interval = Interval(value) } var interval = Interval(note) + set(value) { + field = value + if (note != value.distance) { + note = value.distance + } + } fun getNotes(root: Note): Array { return Array(chordType.notes.size) { root + note + chordType.notes[it] } diff --git a/app/src/main/java/com/lukas/music/song/chords/Interval.kt b/app/src/main/java/com/lukas/music/song/chords/Interval.kt index 2726787..d112805 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Interval.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Interval.kt @@ -10,7 +10,7 @@ package com.lukas.music.song.chords -class Interval(private val distance: Int) { +class Interval(val distance: Int) { val name: IntervalName = when (distance) { 0 -> IntervalName.UNISON 1, 2 -> IntervalName.SECOND @@ -32,7 +32,7 @@ return name.toString() } - enum class IntervalName(val distance: Int, val romanVersion: String) { + enum class IntervalName(private val distance: Int, val romanVersion: String) { UNISON(0, "I"), SECOND(1, "II"), THIRD(3, "III"), diff --git a/app/src/main/java/com/lukas/music/song/note/Note.kt b/app/src/main/java/com/lukas/music/song/note/Note.kt index d3a588d..17bfb2b 100644 --- a/app/src/main/java/com/lukas/music/song/note/Note.kt +++ b/app/src/main/java/com/lukas/music/song/note/Note.kt @@ -28,6 +28,22 @@ return this + (-other) } + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Note + + if (id != other.id) return false + + return true + } + + override fun hashCode(): Int { + return id + } + + companion object { val NOTES = Array(128) { Note(it) } diff --git a/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt b/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt index 3990554..4706068 100644 --- a/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt +++ b/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt @@ -14,9 +14,16 @@ import com.lukas.music.song.note.Note class BassVoice(instrument: Instrument) : Voice(instrument) { - override val steps = listOf(1, 3) + override var noteActive: Array> = arrayOf( + arrayOf(true), + arrayOf(false), + arrayOf(true), + arrayOf(false) + ) - override fun step(root: Note, chord: Array) { - instrument.startNote(chord[0] - 24) + override val noteCount: Int = 1 + + override fun getNotes(root: Note, chordNotes: Array): Array { + return arrayOf(chordNotes[0] - 24) } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/voice/ChordVoice.kt b/app/src/main/java/com/lukas/music/song/voice/ChordVoice.kt index fc5362d..ab7117f 100644 --- a/app/src/main/java/com/lukas/music/song/voice/ChordVoice.kt +++ b/app/src/main/java/com/lukas/music/song/voice/ChordVoice.kt @@ -14,12 +14,15 @@ import com.lukas.music.song.note.Note class ChordVoice(instrument: Instrument) : Voice(instrument) { - override val steps: List = listOf(2, 4) + override var noteActive: Array> = arrayOf( + Array(3) { false }, + Array(3) { true }, + Array(3) { false }, + Array(3) { true }, + ) + override val noteCount: Int = 3 - override fun step(root: Note, chord: Array) { - instrument.stop() - for (note in chord) { - instrument.startNote(note) - } + override fun getNotes(root: Note, chordNotes: Array): Array { + return chordNotes } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/voice/Voice.kt b/app/src/main/java/com/lukas/music/song/voice/Voice.kt index f7fca94..4f56c2a 100644 --- a/app/src/main/java/com/lukas/music/song/voice/Voice.kt +++ b/app/src/main/java/com/lukas/music/song/voice/Voice.kt @@ -12,9 +12,36 @@ import com.lukas.music.instruments.Instrument import com.lukas.music.song.note.Note +import kotlin.reflect.KClass abstract class Voice(val instrument: Instrument) { - abstract val steps: List + abstract var noteActive: Array> + abstract val noteCount: Int - abstract fun step(root: Note, chord: Array) + abstract fun getNotes(root: Note, chordNotes: Array): Array + + fun step(root: Note, chordNotes: Array, beat: Int) { + if (instrument.muted) { + return + } + val activeNotes = noteActive[beat] + val notes = getNotes(root, chordNotes) + for ((index, active) in activeNotes.withIndex()) { + val note = notes[index] + if (!active) { + instrument.stopNote(note) + continue + } + instrument.startNote(note) + } + } + + companion object { + val DEFAULT_VOICES = listOf>( + BassVoice::class, + ChordVoice::class, + ) + + val DEFAULT_VOICE_NAMES = listOf("Bass", "Chord") + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/adapters/InstrumentAdapter.kt b/app/src/main/java/com/lukas/music/ui/adapters/InstrumentAdapter.kt index 8961831..d51f62e 100644 --- a/app/src/main/java/com/lukas/music/ui/adapters/InstrumentAdapter.kt +++ b/app/src/main/java/com/lukas/music/ui/adapters/InstrumentAdapter.kt @@ -11,54 +11,26 @@ package com.lukas.music.ui.adapters import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup -import android.widget.AdapterView -import android.widget.ArrayAdapter import androidx.recyclerview.widget.RecyclerView import com.lukas.music.databinding.FragmentInstrumentBinding import com.lukas.music.instruments.Instrument -import com.lukas.music.instruments.Waveform +import com.lukas.music.ui.fragments.InstrumentListFragment -class InstrumentAdapter : RecyclerView.Adapter() { - class InstrumentViewHolder(val binding: FragmentInstrumentBinding) : - RecyclerView.ViewHolder(binding.root), AdapterView.OnItemSelectedListener { - lateinit var instrument: Instrument - - init { - val adapter = ArrayAdapter( - binding.root.context, - android.R.layout.simple_spinner_dropdown_item, Waveform.VALUES - ) - binding.waveformSelection.adapter = adapter - binding.waveformSelection.onItemSelectedListener = this - } - - override fun onItemSelected( - adapterView: AdapterView<*>?, - view: View?, - position: Int, - id: Long - ) { - instrument.waveform = Waveform.VALUES[position] - } - - override fun onNothingSelected(adapterView: AdapterView<*>?) { - } - } +class InstrumentAdapter(val parent: InstrumentListFragment) : + RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): InstrumentViewHolder { val context = parent.context val inflater = LayoutInflater.from(context) val binding = FragmentInstrumentBinding.inflate(inflater, parent, false) - return InstrumentViewHolder(binding) + return InstrumentViewHolder(binding, this.parent.childFragmentManager, this) } override fun onBindViewHolder(holder: InstrumentViewHolder, position: Int) { val instrument = Instrument.instruments[position] holder.instrument = instrument - instrument.applyToView(holder.binding) } override fun getItemCount(): Int { diff --git a/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt b/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt new file mode 100644 index 0000000..3e129bc --- /dev/null +++ b/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2022 Lukas Eisenhauer + * + * This program 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, either version 3 of the License, or(at your option) any later version. + * + * This program 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 this program. If not, see . + */ + +package com.lukas.music.ui.adapters + +import androidx.fragment.app.FragmentManager +import androidx.recyclerview.widget.RecyclerView +import com.lukas.music.R +import com.lukas.music.databinding.FragmentInstrumentBinding +import com.lukas.music.instruments.Instrument +import com.lukas.music.song.Song +import com.lukas.music.ui.fragments.EditInstrumentFragment +import com.lukas.music.util.setupToggle +import com.lukas.music.util.updateToggle + +class InstrumentViewHolder( + val binding: FragmentInstrumentBinding, + private val childFragmentManager: FragmentManager, + private val adapter: InstrumentAdapter +) : + RecyclerView.ViewHolder(binding.root) { + init { + HOLDERS += this + } + + var solo: Boolean = false + set(value) { + if (value) { + for (holder in HOLDERS) { + holder.solo = false + } + Song.currentSong.soloInstrument = instrument + } + field = value + binding.soloButton.updateToggle(this::solo, R.color.blue) + } + + var instrument: Instrument? = null + set(value) { + field = value + value ?: return + binding.instrumentNameText.text = instrument?.name + binding.editInstrumentButton.setOnClickListener { + EditInstrumentFragment(instrument!!, this).showNow(childFragmentManager, "") + } + binding.muteButton.setupToggle(instrument!!::muted, R.color.red) + binding.soloButton.setupToggle(this::solo, R.color.blue) { + if (!it) { + Song.currentSong.soloInstrument = null + } + } + binding.deleteButton.setOnClickListener { + val index = Instrument.instruments.indexOf(instrument) + Instrument.instruments -= instrument!! + adapter.notifyItemRemoved(index) + instrument!!.destroy() + } + } + + companion object { + val HOLDERS = mutableListOf() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/fragments/EditChordFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/EditChordFragment.kt index a311a6c..bd7a3d9 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/EditChordFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/EditChordFragment.kt @@ -10,13 +10,10 @@ package com.lukas.music.ui.fragments -import android.R import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.AdapterView -import android.widget.ArrayAdapter import androidx.fragment.app.DialogFragment import com.lukas.music.databinding.FragmentEditChordBinding import com.lukas.music.song.Scale @@ -24,8 +21,10 @@ import com.lukas.music.song.chords.Chord import com.lukas.music.song.chords.ChordType import com.lukas.music.song.chords.Interval +import com.lukas.music.util.setup -class EditChordFragment(val chord: Chord, val songFragment: SongFragment) : DialogFragment() { +class EditChordFragment(val chord: Chord, private val songFragment: SongFragment) : + DialogFragment() { lateinit var binding: FragmentEditChordBinding override fun onCreateView( @@ -45,24 +44,12 @@ val pitches = if (songFragment.displayChordNames) { Array(Scale.MAJOR.steps.size) { (Song.currentSong.root + Scale.MAJOR.steps[it]).noteName.toString() } } else Interval.IntervalName.NAMES - val pitchAdapter = ArrayAdapter( - binding.root.context, - R.layout.simple_spinner_dropdown_item, pitches - ) - binding.pitchSpinner.adapter = pitchAdapter - binding.pitchSpinner.setSelection(chord.interval.name.ordinal) - binding.pitchSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { - override fun onItemSelected( - parent: AdapterView<*>?, - view: View?, - position: Int, - id: Long - ) { - chord.note = Scale.MAJOR.steps[position] - songFragment.updateChords() + binding.pitchSpinner.setup(pitches, chord.interval.name.ordinal) { + chord.note = Scale.MAJOR.steps[it] + if (binding.typeSpinner.selectedItemPosition == 0) { + chord.chordType = Scale.MAJOR.chordTypes[chord.interval.name.ordinal] } - - override fun onNothingSelected(parent: AdapterView<*>?) {} + songFragment.updateChords() } } @@ -71,32 +58,17 @@ for (chordType in ChordType.VALUES) { values += chordType.toString() } - val modifierAdapter = ArrayAdapter( - binding.root.context, - R.layout.simple_spinner_dropdown_item, values - ) - binding.typeSpinner.adapter = modifierAdapter - binding.typeSpinner.setSelection( - if (chord.chordType == Scale.MAJOR.chordTypes[chord.interval.name.ordinal]) - 0 + binding.typeSpinner.setup( + values, + if (chord.chordType == Scale.MAJOR.chordTypes[chord.interval.name.ordinal]) 0 else chord.chordType.ordinal + 1 - ) - binding.typeSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { - override fun onItemSelected( - parent: AdapterView<*>?, - view: View?, - position: Int, - id: Long - ) { - if (position == 0) { - chord.chordType = Scale.MAJOR.chordTypes[chord.interval.name.ordinal] - } else { - chord.chordType = ChordType.VALUES[position - 1] - } - songFragment.updateChords() + ) { + if (it == 0) { + chord.chordType = Scale.MAJOR.chordTypes[chord.interval.name.ordinal] + } else { + chord.chordType = ChordType.VALUES[it - 1] } - - override fun onNothingSelected(parent: AdapterView<*>?) {} + songFragment.updateChords() } } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/fragments/EditInstrumentFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/EditInstrumentFragment.kt new file mode 100644 index 0000000..0020ae8 --- /dev/null +++ b/app/src/main/java/com/lukas/music/ui/fragments/EditInstrumentFragment.kt @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2022 Lukas Eisenhauer + * + * This program 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, either version 3 of the License, or(at your option) any later version. + * + * This program 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 this program. If not, see . + */ + +package com.lukas.music.ui.fragments + +import android.os.Bundle +import android.text.Editable +import android.text.TextWatcher +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.DialogFragment +import com.lukas.music.EditVoiceFragment +import com.lukas.music.databinding.FragmentEditInstrumentBinding +import com.lukas.music.instruments.Instrument +import com.lukas.music.instruments.Waveform +import com.lukas.music.song.voice.Voice +import com.lukas.music.ui.adapters.InstrumentViewHolder +import com.lukas.music.util.setup +import com.lukas.music.util.smartSetup + +class EditInstrumentFragment( + private val instrument: Instrument, + private val viewHolder: InstrumentViewHolder +) : DialogFragment() { + lateinit var binding: FragmentEditInstrumentBinding + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEditInstrumentBinding.inflate(inflater) + binding.instrumentNameTextBox.text.clear() + binding.instrumentNameTextBox.text.append(instrument.name) + binding.instrumentNameTextBox.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} + + override fun afterTextChanged(s: Editable?) { + instrument.name = binding.instrumentNameTextBox.text.toString() + viewHolder.instrument = viewHolder.instrument + } + }) + binding.waveformSelection.smartSetup(Waveform.VALUES, instrument::waveform) + binding.volumeSeek.setup(0, 100, 30) { + binding.volumeText.text = "volume: $it%" + instrument.volume = it.toFloat() / 100f + } + binding.voiceSelection.setup( + Voice.DEFAULT_VOICE_NAMES, + Voice.DEFAULT_VOICES.indexOf(instrument.voice::class) + ) { + if (instrument.voice::class == Voice.DEFAULT_VOICES[it]) { + return@setup + } + instrument.voice = Voice.DEFAULT_VOICES[it].constructors.first().call(instrument) + } + binding.editVoiceButton.setOnClickListener { + EditVoiceFragment(instrument.voice).showNow(childFragmentManager, "") + } + binding.closeButton.setOnClickListener { + dismiss() + } + return binding.root + } + + override fun onStart() { + super.onStart() + dialog?.window?.setLayout( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/fragments/InstrumentListFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/InstrumentListFragment.kt index 3f1a910..1d35c10 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/InstrumentListFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/InstrumentListFragment.kt @@ -10,13 +10,19 @@ package com.lukas.music.ui.fragments +import android.app.AlertDialog import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment +import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView import com.lukas.music.databinding.FragmentInstrumentListBinding +import com.lukas.music.instruments.Instrument +import com.lukas.music.instruments.MonoInstrument +import com.lukas.music.instruments.PolyInstrument import com.lukas.music.ui.adapters.InstrumentAdapter class InstrumentListFragment : Fragment() { @@ -27,8 +33,53 @@ savedInstanceState: Bundle? ): View? { binding = FragmentInstrumentListBinding.inflate(inflater) - binding.recyclerView.adapter = InstrumentAdapter() + binding.recyclerView.adapter = InstrumentAdapter(this) binding.recyclerView.layoutManager = LinearLayoutManager(context) + val callback = object : ItemTouchHelper.SimpleCallback( + ItemTouchHelper.UP or ItemTouchHelper.DOWN, + 0 + ) { + override fun onMove( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder + ): Boolean { + val adapter = recyclerView.adapter as InstrumentAdapter + val startPosition = viewHolder.adapterPosition + val endPosition = target.adapterPosition + val instrument = Instrument.instruments[startPosition] + Instrument.instruments.removeAt(startPosition) + if (endPosition < startPosition) { + Instrument.instruments.add(endPosition + 1, instrument) + } else { + Instrument.instruments.add(endPosition - 1, instrument) + } + adapter.notifyItemMoved(startPosition, endPosition) + return true + } + + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {} + } + val helper = ItemTouchHelper(callback) + helper.attachToRecyclerView(binding.recyclerView) + + val builder = AlertDialog.Builder(binding.root.context) + builder.setTitle("Instrument type:") + .setItems( + arrayOf("mono", "poly") + ) { _, index -> + when (index) { + 0 -> Instrument.instruments += MonoInstrument("New mono Instrument") + 1 -> Instrument.instruments += PolyInstrument("New poly Instrument") + } + (binding.recyclerView.adapter as RecyclerView.Adapter).notifyItemInserted( + Instrument.instruments.size - 1 + ) + } + builder.create() + binding.addInstrumentButton.setOnClickListener { + builder.show() + } return binding.root } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt index eaf2c61..0c33019 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt @@ -16,7 +16,10 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.* +import android.widget.RadioButton +import android.widget.RadioGroup +import android.widget.Space +import android.widget.TextView import androidx.cardview.widget.CardView import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment @@ -24,6 +27,7 @@ import com.lukas.music.databinding.FragmentPlayBinding import com.lukas.music.instruments.Rhythm import com.lukas.music.song.Song +import com.lukas.music.util.setup class PlayFragment : Fragment() { private lateinit var binding: FragmentPlayBinding @@ -58,11 +62,11 @@ } Song.currentSong.chordProgression.reverse() } - setupSlider(binding.masterVolumeSlider, 0, 100, 100) { + binding.masterVolumeSlider.setup(0, 100, 100) { setMasterVolume(it.toDouble() / 100.0) binding.masterVolumeText.text = "Master volume: $it%" } - setupSlider(binding.tempoSlider, 50, 150, 90) { + binding.tempoSlider.setup(50, 150, 90) { Rhythm.setTempo(it) binding.tempoText.text = "tempo: ${it}bpm" } @@ -116,33 +120,6 @@ ) } - private fun setupSlider( - slider: SeekBar, - min: Int, - max: Int, - initialProgress: Int, - callback: (Int) -> Unit - ) { - slider.min = min - slider.max = max - slider.setOnSeekBarChangeListener(object : - SeekBar.OnSeekBarChangeListener { - override fun onProgressChanged( - seekBar: SeekBar, - progress: Int, fromUser: Boolean - ) { - callback(progress) - } - - override fun onStartTrackingTouch(seekBar: SeekBar) { - } - - override fun onStopTrackingTouch(seekBar: SeekBar) { - } - }) - slider.progress = initialProgress - } - fun updateChords() { binding.phraseDisplay.removeAllViews() chordDisplays.clear() diff --git a/app/src/main/java/com/lukas/music/util/ArrayProperty.kt b/app/src/main/java/com/lukas/music/util/ArrayProperty.kt new file mode 100644 index 0000000..938f07c --- /dev/null +++ b/app/src/main/java/com/lukas/music/util/ArrayProperty.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2022 Lukas Eisenhauer + * + * This program 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, either version 3 of the License, or(at your option) any later version. + * + * This program 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 this program. If not, see . + */ + +package com.lukas.music.util + +import kotlin.reflect.* + +class ArrayProperty(private val array: Array, private val position: Int) : + KMutableProperty0 { + override val annotations: List = listOf() + override val isAbstract: Boolean = false + override val isFinal: Boolean = false + override val isOpen: Boolean = false + override val isSuspend: Boolean = false + override val name: String = "TODO" + override val parameters: List = listOf() + override val returnType: KType get() = TODO() + override val typeParameters: List = listOf() + override val visibility: KVisibility? = KVisibility.PUBLIC + override fun call(vararg args: Any?): T = TODO() + override fun callBy(args: Map): T = TODO() + override val setter: KMutableProperty0.Setter get() = TODO() + + + override val isConst: Boolean = false + override val isLateinit: Boolean = false + override val getter: KProperty0.Getter get() = TODO() + + + override fun getDelegate(): Any? = null + override fun invoke(): T = TODO() + + override fun set(value: T) { + array[position] = value + } + + + override fun get(): T = array[position] +} diff --git a/.idea/misc.xml b/.idea/misc.xml index 97f8bf5..3d886c4 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,6 +11,8 @@ + + diff --git a/app/build.gradle b/app/build.gradle index 35173c4..97ec2a0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -56,12 +56,12 @@ } dependencies { - implementation 'androidx.core:core-ktx:1.7.0' implementation 'androidx.appcompat:appcompat:1.3.0' implementation 'com.google.android.material:material:1.6.1' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'androidx.gridlayout:gridlayout:1.0.0' + implementation 'org.jetbrains.kotlin:kotlin-reflect:1.7.20-Beta' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index dc80bd1..8e98b44 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -15,6 +15,9 @@ } AudioHost *thiz = static_cast(userData); for (auto const &instrument: *thiz->instruments) { + if (!instrument) { + continue; + } instrument->render(buffer, sampleCount); } for (uint32_t i = 0; i < sampleCount; i++) { diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 6ebd2b0..5505a35 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -8,7 +8,7 @@ static AudioHost *audioHost; -template +template void *listGet(_InputIterator iterator, uint32_t n) { for (uint32_t i = 0; i < n; i++) { iterator++; @@ -16,6 +16,18 @@ return *iterator; } +Instrument *getInstrument(uint32_t id) { + return static_cast(listGet(audioHost->instruments->begin(), id)); +} + +template +void listSet(_InputIterator iterator, uint32_t n, void *value) { + for (uint32_t i = 0; i < n; i++) { + iterator++; + } + *iterator = static_cast(value); +} + extern "C" { JNIEXPORT void JNICALL @@ -23,7 +35,6 @@ audioHost = new AudioHost(); } -extern "C" JNIEXPORT jint JNICALL Java_com_lukas_music_instruments_InternalInstrument_createInstrument(JNIEnv *env, jobject thiz) { uint32_t result = audioHost->instruments->size(); @@ -32,43 +43,37 @@ return result; } -extern "C" -JNIEXPORT void JNICALL -Java_com_lukas_music_instruments_InternalInstrument_setInstrumentActive(JNIEnv *env, jobject thiz, - jint id, jboolean active) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->wave->amplitude = active ? 0.3 : 0.0; -} -} -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_startNote(JNIEnv *env, jobject thiz, jint id, jdouble frequency) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->startNote(frequency); + getInstrument(id)->startNote(frequency); } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_endNote(JNIEnv *env, jobject thiz, jint id) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->endNote(); + getInstrument(id)->endNote(); } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_ui_fragments_PlayFragment_setMasterVolume(JNIEnv *env, jobject thiz, jdouble volume) { audioHost->masterVolume = volume; } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setInstrumentWaveform(JNIEnv *env, jobject thiz, jint id, jint waveform) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->setWaveform(static_cast(waveform)); + getInstrument(id)->setWaveform(static_cast(waveform)); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, + jfloat volume) { + getInstrument(id)->wave->amplitude = volume; +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_destroy(JNIEnv *env, jobject thiz, jint id) { + listSet(audioHost->instruments->begin(), id, nullptr); + delete getInstrument(id); +} } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt new file mode 100644 index 0000000..56f21e6 --- /dev/null +++ b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2022 Lukas Eisenhauer + * + * This program 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, either version 3 of the License, or(at your option) any later version. + * + * This program 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 this program. If not, see . + */ + +package com.lukas.music + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TableRow +import androidx.core.view.setMargins +import androidx.fragment.app.DialogFragment +import com.google.android.material.button.MaterialButton +import com.lukas.music.databinding.FragmentEditVoiceBinding +import com.lukas.music.song.Song +import com.lukas.music.song.voice.Voice +import com.lukas.music.util.ArrayProperty +import com.lukas.music.util.setupToggle + +class EditVoiceFragment(private val voice: Voice) : DialogFragment() { + private lateinit var binding: FragmentEditVoiceBinding + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEditVoiceBinding.inflate(inflater) + for (row in 0 until voice.noteCount) { + val rowLayout = TableRow(binding.root.context) + for (column in 0 until Song.currentSong.beats) { + val button = MaterialButton(binding.root.context) + button.layoutParams = buttonLayout + button.setupToggle(ArrayProperty(voice.noteActive[column], row), R.color.blue) + rowLayout.addView(button) + } + binding.noteGrid.addView(rowLayout) + } + binding.noteGrid.isStretchAllColumns = true + binding.closeButton.setOnClickListener { + dismiss() + } + return binding.root + } + + override fun onStart() { + super.onStart() + dialog?.window?.setLayout( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + } + + companion object { + val buttonLayout = TableRow.LayoutParams(0, TableRow.LayoutParams.WRAP_CONTENT) + + init { + buttonLayout.setMargins(5) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Instrument.kt b/app/src/main/java/com/lukas/music/instruments/Instrument.kt index 8ad90c6..92a896c 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -10,42 +10,22 @@ package com.lukas.music.instruments -import com.lukas.music.databinding.FragmentInstrumentBinding import com.lukas.music.song.note.Note import com.lukas.music.song.voice.BassVoice -import com.lukas.music.song.voice.ChordVoice import com.lukas.music.song.voice.Voice -abstract class Instrument(private var name: String) { - private var active = false +abstract class Instrument(var name: String) { + var voice: Voice = BassVoice(this) abstract var waveform: Waveform - - fun applyToView(binding: FragmentInstrumentBinding) { - binding.instrumentNameText.text = name - binding.editInstrumentButton.setOnClickListener { - println("click instrument $name") - } - binding.activeSwitch.setOnCheckedChangeListener { _, newActive -> - active = newActive - changeActive(newActive) - } - binding.activeSwitch.isChecked = active - } + abstract var volume: Float + abstract var muted: Boolean abstract fun startNote(note: Note) abstract fun stop() - abstract fun changeActive(newActive: Boolean) + abstract fun stopNote(note: Note) + abstract fun destroy() companion object { - val instruments = - mutableListOf( - MonoInstrument("Bass"), - PolyInstrument("Chords"), - ) - - val voice = mutableListOf( - BassVoice(instruments[0]), - ChordVoice(instruments[1]), - ) + val instruments = mutableListOf() } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt index dc64bcc..b4c68a4 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -10,33 +10,67 @@ package com.lukas.music.instruments +import com.lukas.music.song.note.Note + class InternalInstrument { private val id = createInstrument() - var active: Boolean = false - set(value) { - field = value - setInstrumentActive(id, value) - } + var note: Note? = null var waveform: Waveform = Waveform.SINE set(value) { field = value setInstrumentWaveform(id, value.id) - // this is to resend the setInstrumentActive for the new waveform in the internal c++ code - active = active + refresh() } - fun startNote(frequency: Double) { - startNote(id, frequency) + var volume: Float = 0.3f + set(value) { + field = value + if (!muted) { + actualVolume = value + } + } + + var muted: Boolean = false + set(value) { + field = value + actualVolume = if (value) 0.0f else volume + } + + private var actualVolume: Float = 1.0f + set(value) { + field = value + setVolume(id, value) + } + + init { + refresh() + } + + private fun refresh() { + // this is to resend the old information to the internal c++ code (when changing the waveform) + muted = muted + volume = volume + } + + fun startNote(note: Note) { + this.note = note + startNote(id, note.frequency) } fun endNote() { + note = null endNote(id) } + fun destroy() { + destroy(id) + } + private external fun createInstrument(): Int - private external fun setInstrumentActive(id: Int, isActive: Boolean) private external fun setInstrumentWaveform(id: Int, waveform: Int) private external fun startNote(id: Int, frequency: Double) private external fun endNote(id: Int) + private external fun setVolume(id: Int, volume: Float) + private external fun destroy(id: Int) } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt index e7b7464..2b43524 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -21,15 +21,36 @@ internalInstrument.waveform = value } - override fun startNote(note: Note) { - internalInstrument.startNote(note.frequency) - } + override var volume: Float = 1.0f + set(value) { + field = value + internalInstrument.volume = volume + } - override fun changeActive(newActive: Boolean) { - internalInstrument.active = newActive + override var muted: Boolean = false + set(value) { + field = value + internalInstrument.muted = value + } + + override fun startNote(note: Note) { + if (note == internalInstrument.note) { + return + } + internalInstrument.startNote(note) } override fun stop() { internalInstrument.endNote() } + + override fun stopNote(note: Note) { + if (note == internalInstrument.note) { + stop() + } + } + + override fun destroy() { + internalInstrument.destroy() + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt index 9d8b4f9..26e4fbb 100644 --- a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt @@ -24,27 +24,55 @@ } } + override var volume: Float = 1.0f + set(value) { + field = value + for (internalInstrument in internalInstruments) { + internalInstrument.volume = volume + } + } + + override var muted: Boolean = false + set(value) { + field = value + for (instrument in internalInstruments) { + instrument.muted = value + } + } + override fun startNote(note: Note) { for ((index, instrumentPlaying) in playing.withIndex()) { if (!instrumentPlaying) { - internalInstruments[index].startNote(note.frequency) + internalInstruments[index].startNote(note) playing[index] = true return } + if (internalInstruments[index].note == note) { + return + } } throw IllegalStateException("cannot start another note with the current amount of oscillators") } - override fun changeActive(newActive: Boolean) { - for (instrument in internalInstruments) { - instrument.active = newActive - } - } - override fun stop() { for ((i, instrument) in internalInstruments.withIndex()) { instrument.endNote() playing[i] = false } } + + override fun stopNote(note: Note) { + for ((i, instrument) in internalInstruments.withIndex()) { + if (instrument.note == note) { + instrument.endNote() + playing[i] = false + } + } + } + + override fun destroy() { + for (instrument in internalInstruments) { + instrument.destroy() + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/Song.kt b/app/src/main/java/com/lukas/music/song/Song.kt index a3123f2..fc5421b 100644 --- a/app/src/main/java/com/lukas/music/song/Song.kt +++ b/app/src/main/java/com/lukas/music/song/Song.kt @@ -16,10 +16,33 @@ import com.lukas.music.util.Cycle class Song( - var root: Note, + root: Note, val beats: Int ) : Cycle(beats) { val chordProgression = ChordProgression() + var soloInstrument: Instrument? = null + set(value) { + field = value + value?.let { + for (instrument in Instrument.instruments) { + if (instrument != value) { + instrument.stop() + } + } + } + } + + var root: Note = root + set(value) { + field = value + stopAllInstruments() + } + + private fun stopAllInstruments() { + for (instrument in Instrument.instruments) { + instrument.stop() + } + } init { for (i in 0 until beats) { @@ -27,6 +50,7 @@ } wraparoundListeners += { chordProgression.step() + stopAllInstruments() } } @@ -34,8 +58,12 @@ super.step() val chord = chordProgression.currentItem?.currentItem ?: return index val chordNotes = chord.getNotes(root) - for (voice in Instrument.voice) { - voice.step(root, chordNotes) + soloInstrument?.let { + it.voice.step(root, chordNotes, index) + } ?: run { + for (instrument in Instrument.instruments) { + instrument.voice.step(root, chordNotes, index) + } } return index } diff --git a/app/src/main/java/com/lukas/music/song/chords/Chord.kt b/app/src/main/java/com/lukas/music/song/chords/Chord.kt index db45fdd..4400dae 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Chord.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Chord.kt @@ -19,6 +19,12 @@ interval = Interval(value) } var interval = Interval(note) + set(value) { + field = value + if (note != value.distance) { + note = value.distance + } + } fun getNotes(root: Note): Array { return Array(chordType.notes.size) { root + note + chordType.notes[it] } diff --git a/app/src/main/java/com/lukas/music/song/chords/Interval.kt b/app/src/main/java/com/lukas/music/song/chords/Interval.kt index 2726787..d112805 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Interval.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Interval.kt @@ -10,7 +10,7 @@ package com.lukas.music.song.chords -class Interval(private val distance: Int) { +class Interval(val distance: Int) { val name: IntervalName = when (distance) { 0 -> IntervalName.UNISON 1, 2 -> IntervalName.SECOND @@ -32,7 +32,7 @@ return name.toString() } - enum class IntervalName(val distance: Int, val romanVersion: String) { + enum class IntervalName(private val distance: Int, val romanVersion: String) { UNISON(0, "I"), SECOND(1, "II"), THIRD(3, "III"), diff --git a/app/src/main/java/com/lukas/music/song/note/Note.kt b/app/src/main/java/com/lukas/music/song/note/Note.kt index d3a588d..17bfb2b 100644 --- a/app/src/main/java/com/lukas/music/song/note/Note.kt +++ b/app/src/main/java/com/lukas/music/song/note/Note.kt @@ -28,6 +28,22 @@ return this + (-other) } + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Note + + if (id != other.id) return false + + return true + } + + override fun hashCode(): Int { + return id + } + + companion object { val NOTES = Array(128) { Note(it) } diff --git a/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt b/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt index 3990554..4706068 100644 --- a/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt +++ b/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt @@ -14,9 +14,16 @@ import com.lukas.music.song.note.Note class BassVoice(instrument: Instrument) : Voice(instrument) { - override val steps = listOf(1, 3) + override var noteActive: Array> = arrayOf( + arrayOf(true), + arrayOf(false), + arrayOf(true), + arrayOf(false) + ) - override fun step(root: Note, chord: Array) { - instrument.startNote(chord[0] - 24) + override val noteCount: Int = 1 + + override fun getNotes(root: Note, chordNotes: Array): Array { + return arrayOf(chordNotes[0] - 24) } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/voice/ChordVoice.kt b/app/src/main/java/com/lukas/music/song/voice/ChordVoice.kt index fc5362d..ab7117f 100644 --- a/app/src/main/java/com/lukas/music/song/voice/ChordVoice.kt +++ b/app/src/main/java/com/lukas/music/song/voice/ChordVoice.kt @@ -14,12 +14,15 @@ import com.lukas.music.song.note.Note class ChordVoice(instrument: Instrument) : Voice(instrument) { - override val steps: List = listOf(2, 4) + override var noteActive: Array> = arrayOf( + Array(3) { false }, + Array(3) { true }, + Array(3) { false }, + Array(3) { true }, + ) + override val noteCount: Int = 3 - override fun step(root: Note, chord: Array) { - instrument.stop() - for (note in chord) { - instrument.startNote(note) - } + override fun getNotes(root: Note, chordNotes: Array): Array { + return chordNotes } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/voice/Voice.kt b/app/src/main/java/com/lukas/music/song/voice/Voice.kt index f7fca94..4f56c2a 100644 --- a/app/src/main/java/com/lukas/music/song/voice/Voice.kt +++ b/app/src/main/java/com/lukas/music/song/voice/Voice.kt @@ -12,9 +12,36 @@ import com.lukas.music.instruments.Instrument import com.lukas.music.song.note.Note +import kotlin.reflect.KClass abstract class Voice(val instrument: Instrument) { - abstract val steps: List + abstract var noteActive: Array> + abstract val noteCount: Int - abstract fun step(root: Note, chord: Array) + abstract fun getNotes(root: Note, chordNotes: Array): Array + + fun step(root: Note, chordNotes: Array, beat: Int) { + if (instrument.muted) { + return + } + val activeNotes = noteActive[beat] + val notes = getNotes(root, chordNotes) + for ((index, active) in activeNotes.withIndex()) { + val note = notes[index] + if (!active) { + instrument.stopNote(note) + continue + } + instrument.startNote(note) + } + } + + companion object { + val DEFAULT_VOICES = listOf>( + BassVoice::class, + ChordVoice::class, + ) + + val DEFAULT_VOICE_NAMES = listOf("Bass", "Chord") + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/adapters/InstrumentAdapter.kt b/app/src/main/java/com/lukas/music/ui/adapters/InstrumentAdapter.kt index 8961831..d51f62e 100644 --- a/app/src/main/java/com/lukas/music/ui/adapters/InstrumentAdapter.kt +++ b/app/src/main/java/com/lukas/music/ui/adapters/InstrumentAdapter.kt @@ -11,54 +11,26 @@ package com.lukas.music.ui.adapters import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup -import android.widget.AdapterView -import android.widget.ArrayAdapter import androidx.recyclerview.widget.RecyclerView import com.lukas.music.databinding.FragmentInstrumentBinding import com.lukas.music.instruments.Instrument -import com.lukas.music.instruments.Waveform +import com.lukas.music.ui.fragments.InstrumentListFragment -class InstrumentAdapter : RecyclerView.Adapter() { - class InstrumentViewHolder(val binding: FragmentInstrumentBinding) : - RecyclerView.ViewHolder(binding.root), AdapterView.OnItemSelectedListener { - lateinit var instrument: Instrument - - init { - val adapter = ArrayAdapter( - binding.root.context, - android.R.layout.simple_spinner_dropdown_item, Waveform.VALUES - ) - binding.waveformSelection.adapter = adapter - binding.waveformSelection.onItemSelectedListener = this - } - - override fun onItemSelected( - adapterView: AdapterView<*>?, - view: View?, - position: Int, - id: Long - ) { - instrument.waveform = Waveform.VALUES[position] - } - - override fun onNothingSelected(adapterView: AdapterView<*>?) { - } - } +class InstrumentAdapter(val parent: InstrumentListFragment) : + RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): InstrumentViewHolder { val context = parent.context val inflater = LayoutInflater.from(context) val binding = FragmentInstrumentBinding.inflate(inflater, parent, false) - return InstrumentViewHolder(binding) + return InstrumentViewHolder(binding, this.parent.childFragmentManager, this) } override fun onBindViewHolder(holder: InstrumentViewHolder, position: Int) { val instrument = Instrument.instruments[position] holder.instrument = instrument - instrument.applyToView(holder.binding) } override fun getItemCount(): Int { diff --git a/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt b/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt new file mode 100644 index 0000000..3e129bc --- /dev/null +++ b/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2022 Lukas Eisenhauer + * + * This program 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, either version 3 of the License, or(at your option) any later version. + * + * This program 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 this program. If not, see . + */ + +package com.lukas.music.ui.adapters + +import androidx.fragment.app.FragmentManager +import androidx.recyclerview.widget.RecyclerView +import com.lukas.music.R +import com.lukas.music.databinding.FragmentInstrumentBinding +import com.lukas.music.instruments.Instrument +import com.lukas.music.song.Song +import com.lukas.music.ui.fragments.EditInstrumentFragment +import com.lukas.music.util.setupToggle +import com.lukas.music.util.updateToggle + +class InstrumentViewHolder( + val binding: FragmentInstrumentBinding, + private val childFragmentManager: FragmentManager, + private val adapter: InstrumentAdapter +) : + RecyclerView.ViewHolder(binding.root) { + init { + HOLDERS += this + } + + var solo: Boolean = false + set(value) { + if (value) { + for (holder in HOLDERS) { + holder.solo = false + } + Song.currentSong.soloInstrument = instrument + } + field = value + binding.soloButton.updateToggle(this::solo, R.color.blue) + } + + var instrument: Instrument? = null + set(value) { + field = value + value ?: return + binding.instrumentNameText.text = instrument?.name + binding.editInstrumentButton.setOnClickListener { + EditInstrumentFragment(instrument!!, this).showNow(childFragmentManager, "") + } + binding.muteButton.setupToggle(instrument!!::muted, R.color.red) + binding.soloButton.setupToggle(this::solo, R.color.blue) { + if (!it) { + Song.currentSong.soloInstrument = null + } + } + binding.deleteButton.setOnClickListener { + val index = Instrument.instruments.indexOf(instrument) + Instrument.instruments -= instrument!! + adapter.notifyItemRemoved(index) + instrument!!.destroy() + } + } + + companion object { + val HOLDERS = mutableListOf() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/fragments/EditChordFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/EditChordFragment.kt index a311a6c..bd7a3d9 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/EditChordFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/EditChordFragment.kt @@ -10,13 +10,10 @@ package com.lukas.music.ui.fragments -import android.R import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.AdapterView -import android.widget.ArrayAdapter import androidx.fragment.app.DialogFragment import com.lukas.music.databinding.FragmentEditChordBinding import com.lukas.music.song.Scale @@ -24,8 +21,10 @@ import com.lukas.music.song.chords.Chord import com.lukas.music.song.chords.ChordType import com.lukas.music.song.chords.Interval +import com.lukas.music.util.setup -class EditChordFragment(val chord: Chord, val songFragment: SongFragment) : DialogFragment() { +class EditChordFragment(val chord: Chord, private val songFragment: SongFragment) : + DialogFragment() { lateinit var binding: FragmentEditChordBinding override fun onCreateView( @@ -45,24 +44,12 @@ val pitches = if (songFragment.displayChordNames) { Array(Scale.MAJOR.steps.size) { (Song.currentSong.root + Scale.MAJOR.steps[it]).noteName.toString() } } else Interval.IntervalName.NAMES - val pitchAdapter = ArrayAdapter( - binding.root.context, - R.layout.simple_spinner_dropdown_item, pitches - ) - binding.pitchSpinner.adapter = pitchAdapter - binding.pitchSpinner.setSelection(chord.interval.name.ordinal) - binding.pitchSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { - override fun onItemSelected( - parent: AdapterView<*>?, - view: View?, - position: Int, - id: Long - ) { - chord.note = Scale.MAJOR.steps[position] - songFragment.updateChords() + binding.pitchSpinner.setup(pitches, chord.interval.name.ordinal) { + chord.note = Scale.MAJOR.steps[it] + if (binding.typeSpinner.selectedItemPosition == 0) { + chord.chordType = Scale.MAJOR.chordTypes[chord.interval.name.ordinal] } - - override fun onNothingSelected(parent: AdapterView<*>?) {} + songFragment.updateChords() } } @@ -71,32 +58,17 @@ for (chordType in ChordType.VALUES) { values += chordType.toString() } - val modifierAdapter = ArrayAdapter( - binding.root.context, - R.layout.simple_spinner_dropdown_item, values - ) - binding.typeSpinner.adapter = modifierAdapter - binding.typeSpinner.setSelection( - if (chord.chordType == Scale.MAJOR.chordTypes[chord.interval.name.ordinal]) - 0 + binding.typeSpinner.setup( + values, + if (chord.chordType == Scale.MAJOR.chordTypes[chord.interval.name.ordinal]) 0 else chord.chordType.ordinal + 1 - ) - binding.typeSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { - override fun onItemSelected( - parent: AdapterView<*>?, - view: View?, - position: Int, - id: Long - ) { - if (position == 0) { - chord.chordType = Scale.MAJOR.chordTypes[chord.interval.name.ordinal] - } else { - chord.chordType = ChordType.VALUES[position - 1] - } - songFragment.updateChords() + ) { + if (it == 0) { + chord.chordType = Scale.MAJOR.chordTypes[chord.interval.name.ordinal] + } else { + chord.chordType = ChordType.VALUES[it - 1] } - - override fun onNothingSelected(parent: AdapterView<*>?) {} + songFragment.updateChords() } } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/fragments/EditInstrumentFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/EditInstrumentFragment.kt new file mode 100644 index 0000000..0020ae8 --- /dev/null +++ b/app/src/main/java/com/lukas/music/ui/fragments/EditInstrumentFragment.kt @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2022 Lukas Eisenhauer + * + * This program 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, either version 3 of the License, or(at your option) any later version. + * + * This program 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 this program. If not, see . + */ + +package com.lukas.music.ui.fragments + +import android.os.Bundle +import android.text.Editable +import android.text.TextWatcher +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.DialogFragment +import com.lukas.music.EditVoiceFragment +import com.lukas.music.databinding.FragmentEditInstrumentBinding +import com.lukas.music.instruments.Instrument +import com.lukas.music.instruments.Waveform +import com.lukas.music.song.voice.Voice +import com.lukas.music.ui.adapters.InstrumentViewHolder +import com.lukas.music.util.setup +import com.lukas.music.util.smartSetup + +class EditInstrumentFragment( + private val instrument: Instrument, + private val viewHolder: InstrumentViewHolder +) : DialogFragment() { + lateinit var binding: FragmentEditInstrumentBinding + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEditInstrumentBinding.inflate(inflater) + binding.instrumentNameTextBox.text.clear() + binding.instrumentNameTextBox.text.append(instrument.name) + binding.instrumentNameTextBox.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} + + override fun afterTextChanged(s: Editable?) { + instrument.name = binding.instrumentNameTextBox.text.toString() + viewHolder.instrument = viewHolder.instrument + } + }) + binding.waveformSelection.smartSetup(Waveform.VALUES, instrument::waveform) + binding.volumeSeek.setup(0, 100, 30) { + binding.volumeText.text = "volume: $it%" + instrument.volume = it.toFloat() / 100f + } + binding.voiceSelection.setup( + Voice.DEFAULT_VOICE_NAMES, + Voice.DEFAULT_VOICES.indexOf(instrument.voice::class) + ) { + if (instrument.voice::class == Voice.DEFAULT_VOICES[it]) { + return@setup + } + instrument.voice = Voice.DEFAULT_VOICES[it].constructors.first().call(instrument) + } + binding.editVoiceButton.setOnClickListener { + EditVoiceFragment(instrument.voice).showNow(childFragmentManager, "") + } + binding.closeButton.setOnClickListener { + dismiss() + } + return binding.root + } + + override fun onStart() { + super.onStart() + dialog?.window?.setLayout( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/fragments/InstrumentListFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/InstrumentListFragment.kt index 3f1a910..1d35c10 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/InstrumentListFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/InstrumentListFragment.kt @@ -10,13 +10,19 @@ package com.lukas.music.ui.fragments +import android.app.AlertDialog import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment +import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView import com.lukas.music.databinding.FragmentInstrumentListBinding +import com.lukas.music.instruments.Instrument +import com.lukas.music.instruments.MonoInstrument +import com.lukas.music.instruments.PolyInstrument import com.lukas.music.ui.adapters.InstrumentAdapter class InstrumentListFragment : Fragment() { @@ -27,8 +33,53 @@ savedInstanceState: Bundle? ): View? { binding = FragmentInstrumentListBinding.inflate(inflater) - binding.recyclerView.adapter = InstrumentAdapter() + binding.recyclerView.adapter = InstrumentAdapter(this) binding.recyclerView.layoutManager = LinearLayoutManager(context) + val callback = object : ItemTouchHelper.SimpleCallback( + ItemTouchHelper.UP or ItemTouchHelper.DOWN, + 0 + ) { + override fun onMove( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder + ): Boolean { + val adapter = recyclerView.adapter as InstrumentAdapter + val startPosition = viewHolder.adapterPosition + val endPosition = target.adapterPosition + val instrument = Instrument.instruments[startPosition] + Instrument.instruments.removeAt(startPosition) + if (endPosition < startPosition) { + Instrument.instruments.add(endPosition + 1, instrument) + } else { + Instrument.instruments.add(endPosition - 1, instrument) + } + adapter.notifyItemMoved(startPosition, endPosition) + return true + } + + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {} + } + val helper = ItemTouchHelper(callback) + helper.attachToRecyclerView(binding.recyclerView) + + val builder = AlertDialog.Builder(binding.root.context) + builder.setTitle("Instrument type:") + .setItems( + arrayOf("mono", "poly") + ) { _, index -> + when (index) { + 0 -> Instrument.instruments += MonoInstrument("New mono Instrument") + 1 -> Instrument.instruments += PolyInstrument("New poly Instrument") + } + (binding.recyclerView.adapter as RecyclerView.Adapter).notifyItemInserted( + Instrument.instruments.size - 1 + ) + } + builder.create() + binding.addInstrumentButton.setOnClickListener { + builder.show() + } return binding.root } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt index eaf2c61..0c33019 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt @@ -16,7 +16,10 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.* +import android.widget.RadioButton +import android.widget.RadioGroup +import android.widget.Space +import android.widget.TextView import androidx.cardview.widget.CardView import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment @@ -24,6 +27,7 @@ import com.lukas.music.databinding.FragmentPlayBinding import com.lukas.music.instruments.Rhythm import com.lukas.music.song.Song +import com.lukas.music.util.setup class PlayFragment : Fragment() { private lateinit var binding: FragmentPlayBinding @@ -58,11 +62,11 @@ } Song.currentSong.chordProgression.reverse() } - setupSlider(binding.masterVolumeSlider, 0, 100, 100) { + binding.masterVolumeSlider.setup(0, 100, 100) { setMasterVolume(it.toDouble() / 100.0) binding.masterVolumeText.text = "Master volume: $it%" } - setupSlider(binding.tempoSlider, 50, 150, 90) { + binding.tempoSlider.setup(50, 150, 90) { Rhythm.setTempo(it) binding.tempoText.text = "tempo: ${it}bpm" } @@ -116,33 +120,6 @@ ) } - private fun setupSlider( - slider: SeekBar, - min: Int, - max: Int, - initialProgress: Int, - callback: (Int) -> Unit - ) { - slider.min = min - slider.max = max - slider.setOnSeekBarChangeListener(object : - SeekBar.OnSeekBarChangeListener { - override fun onProgressChanged( - seekBar: SeekBar, - progress: Int, fromUser: Boolean - ) { - callback(progress) - } - - override fun onStartTrackingTouch(seekBar: SeekBar) { - } - - override fun onStopTrackingTouch(seekBar: SeekBar) { - } - }) - slider.progress = initialProgress - } - fun updateChords() { binding.phraseDisplay.removeAllViews() chordDisplays.clear() diff --git a/app/src/main/java/com/lukas/music/util/ArrayProperty.kt b/app/src/main/java/com/lukas/music/util/ArrayProperty.kt new file mode 100644 index 0000000..938f07c --- /dev/null +++ b/app/src/main/java/com/lukas/music/util/ArrayProperty.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2022 Lukas Eisenhauer + * + * This program 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, either version 3 of the License, or(at your option) any later version. + * + * This program 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 this program. If not, see . + */ + +package com.lukas.music.util + +import kotlin.reflect.* + +class ArrayProperty(private val array: Array, private val position: Int) : + KMutableProperty0 { + override val annotations: List = listOf() + override val isAbstract: Boolean = false + override val isFinal: Boolean = false + override val isOpen: Boolean = false + override val isSuspend: Boolean = false + override val name: String = "TODO" + override val parameters: List = listOf() + override val returnType: KType get() = TODO() + override val typeParameters: List = listOf() + override val visibility: KVisibility? = KVisibility.PUBLIC + override fun call(vararg args: Any?): T = TODO() + override fun callBy(args: Map): T = TODO() + override val setter: KMutableProperty0.Setter get() = TODO() + + + override val isConst: Boolean = false + override val isLateinit: Boolean = false + override val getter: KProperty0.Getter get() = TODO() + + + override fun getDelegate(): Any? = null + override fun invoke(): T = TODO() + + override fun set(value: T) { + array[position] = value + } + + + override fun get(): T = array[position] +} diff --git a/app/src/main/java/com/lukas/music/util/UIUtil.kt b/app/src/main/java/com/lukas/music/util/UIUtil.kt new file mode 100644 index 0000000..da897c5 --- /dev/null +++ b/app/src/main/java/com/lukas/music/util/UIUtil.kt @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2022 Lukas Eisenhauer + * + * This program 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, either version 3 of the License, or(at your option) any later version. + * + * This program 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 this program. If not, see . + */ + +package com.lukas.music.util + +import android.view.View +import android.widget.* +import androidx.core.content.ContextCompat +import com.lukas.music.R +import kotlin.reflect.KMutableProperty0 + +fun SeekBar.setup( + min: Int, max: Int, initialProgress: Int, callback: (Int) -> Unit +) { + this.min = min + this.max = max + setOnSeekBarChangeListener(object : + SeekBar.OnSeekBarChangeListener { + override fun onProgressChanged( + seekBar: SeekBar, + progress: Int, fromUser: Boolean + ) { + callback(progress) + } + + override fun onStartTrackingTouch(seekBar: SeekBar) { + } + + override fun onStopTrackingTouch(seekBar: SeekBar) { + } + }) + this.progress = initialProgress +} + +fun Button.setupToggle( + target: KMutableProperty0, + activeColor: Int, + callback: (Boolean) -> Unit = {} +) { + setOnClickListener { + target.set(!target.get()) + updateToggle(target, activeColor) + callback(target.get()) + } + updateToggle(target, activeColor) +} + +fun Button.updateToggle(target: KMutableProperty0, activeColor: Int) { + setBackgroundColor( + ContextCompat.getColor(context, if (target.get()) activeColor else R.color.gray_0x60) + ) +} + +fun Spinner.setup( + items: Array, + initialIndex: Int, + callback: (Int) -> Unit = {}, +) { + val arrayAdapter = ArrayAdapter( + context, + android.R.layout.simple_spinner_dropdown_item, items + ) + spinnerSetupMain(arrayAdapter, initialIndex, callback) +} + +fun Spinner.setup( + items: List, + initialIndex: Int, + callback: (Int) -> Unit = {}, +) { + val arrayAdapter = ArrayAdapter( + context, + android.R.layout.simple_spinner_dropdown_item, items + ) + spinnerSetupMain(arrayAdapter, initialIndex, callback) +} + +private fun Spinner.spinnerSetupMain( + arrayAdapter: ArrayAdapter, + initialIndex: Int, + callback: (Int) -> Unit +) { + adapter = arrayAdapter + setSelection(initialIndex) + onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onItemSelected( + parent: AdapterView<*>?, + view: View?, + position: Int, + id: Long + ) { + callback(position) + } + + override fun onNothingSelected(parent: AdapterView<*>?) {} + } +} + +fun > Spinner.smartSetup( + items: Array, + target: KMutableProperty0, + callback: (Int) -> Unit = {} +) { + val arrayAdapter = ArrayAdapter( + context, + android.R.layout.simple_spinner_dropdown_item, items + ) + spinnerSetupMain(arrayAdapter, target.get().ordinal) { + if (target.get() != items[it]) { + target.set(items[it]) + } + callback(it) + } +} \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 97f8bf5..3d886c4 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,6 +11,8 @@ + + diff --git a/app/build.gradle b/app/build.gradle index 35173c4..97ec2a0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -56,12 +56,12 @@ } dependencies { - implementation 'androidx.core:core-ktx:1.7.0' implementation 'androidx.appcompat:appcompat:1.3.0' implementation 'com.google.android.material:material:1.6.1' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'androidx.gridlayout:gridlayout:1.0.0' + implementation 'org.jetbrains.kotlin:kotlin-reflect:1.7.20-Beta' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index dc80bd1..8e98b44 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -15,6 +15,9 @@ } AudioHost *thiz = static_cast(userData); for (auto const &instrument: *thiz->instruments) { + if (!instrument) { + continue; + } instrument->render(buffer, sampleCount); } for (uint32_t i = 0; i < sampleCount; i++) { diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 6ebd2b0..5505a35 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -8,7 +8,7 @@ static AudioHost *audioHost; -template +template void *listGet(_InputIterator iterator, uint32_t n) { for (uint32_t i = 0; i < n; i++) { iterator++; @@ -16,6 +16,18 @@ return *iterator; } +Instrument *getInstrument(uint32_t id) { + return static_cast(listGet(audioHost->instruments->begin(), id)); +} + +template +void listSet(_InputIterator iterator, uint32_t n, void *value) { + for (uint32_t i = 0; i < n; i++) { + iterator++; + } + *iterator = static_cast(value); +} + extern "C" { JNIEXPORT void JNICALL @@ -23,7 +35,6 @@ audioHost = new AudioHost(); } -extern "C" JNIEXPORT jint JNICALL Java_com_lukas_music_instruments_InternalInstrument_createInstrument(JNIEnv *env, jobject thiz) { uint32_t result = audioHost->instruments->size(); @@ -32,43 +43,37 @@ return result; } -extern "C" -JNIEXPORT void JNICALL -Java_com_lukas_music_instruments_InternalInstrument_setInstrumentActive(JNIEnv *env, jobject thiz, - jint id, jboolean active) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->wave->amplitude = active ? 0.3 : 0.0; -} -} -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_startNote(JNIEnv *env, jobject thiz, jint id, jdouble frequency) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->startNote(frequency); + getInstrument(id)->startNote(frequency); } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_endNote(JNIEnv *env, jobject thiz, jint id) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->endNote(); + getInstrument(id)->endNote(); } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_ui_fragments_PlayFragment_setMasterVolume(JNIEnv *env, jobject thiz, jdouble volume) { audioHost->masterVolume = volume; } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setInstrumentWaveform(JNIEnv *env, jobject thiz, jint id, jint waveform) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->setWaveform(static_cast(waveform)); + getInstrument(id)->setWaveform(static_cast(waveform)); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, + jfloat volume) { + getInstrument(id)->wave->amplitude = volume; +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_destroy(JNIEnv *env, jobject thiz, jint id) { + listSet(audioHost->instruments->begin(), id, nullptr); + delete getInstrument(id); +} } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt new file mode 100644 index 0000000..56f21e6 --- /dev/null +++ b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2022 Lukas Eisenhauer + * + * This program 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, either version 3 of the License, or(at your option) any later version. + * + * This program 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 this program. If not, see . + */ + +package com.lukas.music + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TableRow +import androidx.core.view.setMargins +import androidx.fragment.app.DialogFragment +import com.google.android.material.button.MaterialButton +import com.lukas.music.databinding.FragmentEditVoiceBinding +import com.lukas.music.song.Song +import com.lukas.music.song.voice.Voice +import com.lukas.music.util.ArrayProperty +import com.lukas.music.util.setupToggle + +class EditVoiceFragment(private val voice: Voice) : DialogFragment() { + private lateinit var binding: FragmentEditVoiceBinding + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEditVoiceBinding.inflate(inflater) + for (row in 0 until voice.noteCount) { + val rowLayout = TableRow(binding.root.context) + for (column in 0 until Song.currentSong.beats) { + val button = MaterialButton(binding.root.context) + button.layoutParams = buttonLayout + button.setupToggle(ArrayProperty(voice.noteActive[column], row), R.color.blue) + rowLayout.addView(button) + } + binding.noteGrid.addView(rowLayout) + } + binding.noteGrid.isStretchAllColumns = true + binding.closeButton.setOnClickListener { + dismiss() + } + return binding.root + } + + override fun onStart() { + super.onStart() + dialog?.window?.setLayout( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + } + + companion object { + val buttonLayout = TableRow.LayoutParams(0, TableRow.LayoutParams.WRAP_CONTENT) + + init { + buttonLayout.setMargins(5) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Instrument.kt b/app/src/main/java/com/lukas/music/instruments/Instrument.kt index 8ad90c6..92a896c 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -10,42 +10,22 @@ package com.lukas.music.instruments -import com.lukas.music.databinding.FragmentInstrumentBinding import com.lukas.music.song.note.Note import com.lukas.music.song.voice.BassVoice -import com.lukas.music.song.voice.ChordVoice import com.lukas.music.song.voice.Voice -abstract class Instrument(private var name: String) { - private var active = false +abstract class Instrument(var name: String) { + var voice: Voice = BassVoice(this) abstract var waveform: Waveform - - fun applyToView(binding: FragmentInstrumentBinding) { - binding.instrumentNameText.text = name - binding.editInstrumentButton.setOnClickListener { - println("click instrument $name") - } - binding.activeSwitch.setOnCheckedChangeListener { _, newActive -> - active = newActive - changeActive(newActive) - } - binding.activeSwitch.isChecked = active - } + abstract var volume: Float + abstract var muted: Boolean abstract fun startNote(note: Note) abstract fun stop() - abstract fun changeActive(newActive: Boolean) + abstract fun stopNote(note: Note) + abstract fun destroy() companion object { - val instruments = - mutableListOf( - MonoInstrument("Bass"), - PolyInstrument("Chords"), - ) - - val voice = mutableListOf( - BassVoice(instruments[0]), - ChordVoice(instruments[1]), - ) + val instruments = mutableListOf() } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt index dc64bcc..b4c68a4 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -10,33 +10,67 @@ package com.lukas.music.instruments +import com.lukas.music.song.note.Note + class InternalInstrument { private val id = createInstrument() - var active: Boolean = false - set(value) { - field = value - setInstrumentActive(id, value) - } + var note: Note? = null var waveform: Waveform = Waveform.SINE set(value) { field = value setInstrumentWaveform(id, value.id) - // this is to resend the setInstrumentActive for the new waveform in the internal c++ code - active = active + refresh() } - fun startNote(frequency: Double) { - startNote(id, frequency) + var volume: Float = 0.3f + set(value) { + field = value + if (!muted) { + actualVolume = value + } + } + + var muted: Boolean = false + set(value) { + field = value + actualVolume = if (value) 0.0f else volume + } + + private var actualVolume: Float = 1.0f + set(value) { + field = value + setVolume(id, value) + } + + init { + refresh() + } + + private fun refresh() { + // this is to resend the old information to the internal c++ code (when changing the waveform) + muted = muted + volume = volume + } + + fun startNote(note: Note) { + this.note = note + startNote(id, note.frequency) } fun endNote() { + note = null endNote(id) } + fun destroy() { + destroy(id) + } + private external fun createInstrument(): Int - private external fun setInstrumentActive(id: Int, isActive: Boolean) private external fun setInstrumentWaveform(id: Int, waveform: Int) private external fun startNote(id: Int, frequency: Double) private external fun endNote(id: Int) + private external fun setVolume(id: Int, volume: Float) + private external fun destroy(id: Int) } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt index e7b7464..2b43524 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -21,15 +21,36 @@ internalInstrument.waveform = value } - override fun startNote(note: Note) { - internalInstrument.startNote(note.frequency) - } + override var volume: Float = 1.0f + set(value) { + field = value + internalInstrument.volume = volume + } - override fun changeActive(newActive: Boolean) { - internalInstrument.active = newActive + override var muted: Boolean = false + set(value) { + field = value + internalInstrument.muted = value + } + + override fun startNote(note: Note) { + if (note == internalInstrument.note) { + return + } + internalInstrument.startNote(note) } override fun stop() { internalInstrument.endNote() } + + override fun stopNote(note: Note) { + if (note == internalInstrument.note) { + stop() + } + } + + override fun destroy() { + internalInstrument.destroy() + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt index 9d8b4f9..26e4fbb 100644 --- a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt @@ -24,27 +24,55 @@ } } + override var volume: Float = 1.0f + set(value) { + field = value + for (internalInstrument in internalInstruments) { + internalInstrument.volume = volume + } + } + + override var muted: Boolean = false + set(value) { + field = value + for (instrument in internalInstruments) { + instrument.muted = value + } + } + override fun startNote(note: Note) { for ((index, instrumentPlaying) in playing.withIndex()) { if (!instrumentPlaying) { - internalInstruments[index].startNote(note.frequency) + internalInstruments[index].startNote(note) playing[index] = true return } + if (internalInstruments[index].note == note) { + return + } } throw IllegalStateException("cannot start another note with the current amount of oscillators") } - override fun changeActive(newActive: Boolean) { - for (instrument in internalInstruments) { - instrument.active = newActive - } - } - override fun stop() { for ((i, instrument) in internalInstruments.withIndex()) { instrument.endNote() playing[i] = false } } + + override fun stopNote(note: Note) { + for ((i, instrument) in internalInstruments.withIndex()) { + if (instrument.note == note) { + instrument.endNote() + playing[i] = false + } + } + } + + override fun destroy() { + for (instrument in internalInstruments) { + instrument.destroy() + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/Song.kt b/app/src/main/java/com/lukas/music/song/Song.kt index a3123f2..fc5421b 100644 --- a/app/src/main/java/com/lukas/music/song/Song.kt +++ b/app/src/main/java/com/lukas/music/song/Song.kt @@ -16,10 +16,33 @@ import com.lukas.music.util.Cycle class Song( - var root: Note, + root: Note, val beats: Int ) : Cycle(beats) { val chordProgression = ChordProgression() + var soloInstrument: Instrument? = null + set(value) { + field = value + value?.let { + for (instrument in Instrument.instruments) { + if (instrument != value) { + instrument.stop() + } + } + } + } + + var root: Note = root + set(value) { + field = value + stopAllInstruments() + } + + private fun stopAllInstruments() { + for (instrument in Instrument.instruments) { + instrument.stop() + } + } init { for (i in 0 until beats) { @@ -27,6 +50,7 @@ } wraparoundListeners += { chordProgression.step() + stopAllInstruments() } } @@ -34,8 +58,12 @@ super.step() val chord = chordProgression.currentItem?.currentItem ?: return index val chordNotes = chord.getNotes(root) - for (voice in Instrument.voice) { - voice.step(root, chordNotes) + soloInstrument?.let { + it.voice.step(root, chordNotes, index) + } ?: run { + for (instrument in Instrument.instruments) { + instrument.voice.step(root, chordNotes, index) + } } return index } diff --git a/app/src/main/java/com/lukas/music/song/chords/Chord.kt b/app/src/main/java/com/lukas/music/song/chords/Chord.kt index db45fdd..4400dae 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Chord.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Chord.kt @@ -19,6 +19,12 @@ interval = Interval(value) } var interval = Interval(note) + set(value) { + field = value + if (note != value.distance) { + note = value.distance + } + } fun getNotes(root: Note): Array { return Array(chordType.notes.size) { root + note + chordType.notes[it] } diff --git a/app/src/main/java/com/lukas/music/song/chords/Interval.kt b/app/src/main/java/com/lukas/music/song/chords/Interval.kt index 2726787..d112805 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Interval.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Interval.kt @@ -10,7 +10,7 @@ package com.lukas.music.song.chords -class Interval(private val distance: Int) { +class Interval(val distance: Int) { val name: IntervalName = when (distance) { 0 -> IntervalName.UNISON 1, 2 -> IntervalName.SECOND @@ -32,7 +32,7 @@ return name.toString() } - enum class IntervalName(val distance: Int, val romanVersion: String) { + enum class IntervalName(private val distance: Int, val romanVersion: String) { UNISON(0, "I"), SECOND(1, "II"), THIRD(3, "III"), diff --git a/app/src/main/java/com/lukas/music/song/note/Note.kt b/app/src/main/java/com/lukas/music/song/note/Note.kt index d3a588d..17bfb2b 100644 --- a/app/src/main/java/com/lukas/music/song/note/Note.kt +++ b/app/src/main/java/com/lukas/music/song/note/Note.kt @@ -28,6 +28,22 @@ return this + (-other) } + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Note + + if (id != other.id) return false + + return true + } + + override fun hashCode(): Int { + return id + } + + companion object { val NOTES = Array(128) { Note(it) } diff --git a/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt b/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt index 3990554..4706068 100644 --- a/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt +++ b/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt @@ -14,9 +14,16 @@ import com.lukas.music.song.note.Note class BassVoice(instrument: Instrument) : Voice(instrument) { - override val steps = listOf(1, 3) + override var noteActive: Array> = arrayOf( + arrayOf(true), + arrayOf(false), + arrayOf(true), + arrayOf(false) + ) - override fun step(root: Note, chord: Array) { - instrument.startNote(chord[0] - 24) + override val noteCount: Int = 1 + + override fun getNotes(root: Note, chordNotes: Array): Array { + return arrayOf(chordNotes[0] - 24) } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/voice/ChordVoice.kt b/app/src/main/java/com/lukas/music/song/voice/ChordVoice.kt index fc5362d..ab7117f 100644 --- a/app/src/main/java/com/lukas/music/song/voice/ChordVoice.kt +++ b/app/src/main/java/com/lukas/music/song/voice/ChordVoice.kt @@ -14,12 +14,15 @@ import com.lukas.music.song.note.Note class ChordVoice(instrument: Instrument) : Voice(instrument) { - override val steps: List = listOf(2, 4) + override var noteActive: Array> = arrayOf( + Array(3) { false }, + Array(3) { true }, + Array(3) { false }, + Array(3) { true }, + ) + override val noteCount: Int = 3 - override fun step(root: Note, chord: Array) { - instrument.stop() - for (note in chord) { - instrument.startNote(note) - } + override fun getNotes(root: Note, chordNotes: Array): Array { + return chordNotes } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/voice/Voice.kt b/app/src/main/java/com/lukas/music/song/voice/Voice.kt index f7fca94..4f56c2a 100644 --- a/app/src/main/java/com/lukas/music/song/voice/Voice.kt +++ b/app/src/main/java/com/lukas/music/song/voice/Voice.kt @@ -12,9 +12,36 @@ import com.lukas.music.instruments.Instrument import com.lukas.music.song.note.Note +import kotlin.reflect.KClass abstract class Voice(val instrument: Instrument) { - abstract val steps: List + abstract var noteActive: Array> + abstract val noteCount: Int - abstract fun step(root: Note, chord: Array) + abstract fun getNotes(root: Note, chordNotes: Array): Array + + fun step(root: Note, chordNotes: Array, beat: Int) { + if (instrument.muted) { + return + } + val activeNotes = noteActive[beat] + val notes = getNotes(root, chordNotes) + for ((index, active) in activeNotes.withIndex()) { + val note = notes[index] + if (!active) { + instrument.stopNote(note) + continue + } + instrument.startNote(note) + } + } + + companion object { + val DEFAULT_VOICES = listOf>( + BassVoice::class, + ChordVoice::class, + ) + + val DEFAULT_VOICE_NAMES = listOf("Bass", "Chord") + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/adapters/InstrumentAdapter.kt b/app/src/main/java/com/lukas/music/ui/adapters/InstrumentAdapter.kt index 8961831..d51f62e 100644 --- a/app/src/main/java/com/lukas/music/ui/adapters/InstrumentAdapter.kt +++ b/app/src/main/java/com/lukas/music/ui/adapters/InstrumentAdapter.kt @@ -11,54 +11,26 @@ package com.lukas.music.ui.adapters import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup -import android.widget.AdapterView -import android.widget.ArrayAdapter import androidx.recyclerview.widget.RecyclerView import com.lukas.music.databinding.FragmentInstrumentBinding import com.lukas.music.instruments.Instrument -import com.lukas.music.instruments.Waveform +import com.lukas.music.ui.fragments.InstrumentListFragment -class InstrumentAdapter : RecyclerView.Adapter() { - class InstrumentViewHolder(val binding: FragmentInstrumentBinding) : - RecyclerView.ViewHolder(binding.root), AdapterView.OnItemSelectedListener { - lateinit var instrument: Instrument - - init { - val adapter = ArrayAdapter( - binding.root.context, - android.R.layout.simple_spinner_dropdown_item, Waveform.VALUES - ) - binding.waveformSelection.adapter = adapter - binding.waveformSelection.onItemSelectedListener = this - } - - override fun onItemSelected( - adapterView: AdapterView<*>?, - view: View?, - position: Int, - id: Long - ) { - instrument.waveform = Waveform.VALUES[position] - } - - override fun onNothingSelected(adapterView: AdapterView<*>?) { - } - } +class InstrumentAdapter(val parent: InstrumentListFragment) : + RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): InstrumentViewHolder { val context = parent.context val inflater = LayoutInflater.from(context) val binding = FragmentInstrumentBinding.inflate(inflater, parent, false) - return InstrumentViewHolder(binding) + return InstrumentViewHolder(binding, this.parent.childFragmentManager, this) } override fun onBindViewHolder(holder: InstrumentViewHolder, position: Int) { val instrument = Instrument.instruments[position] holder.instrument = instrument - instrument.applyToView(holder.binding) } override fun getItemCount(): Int { diff --git a/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt b/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt new file mode 100644 index 0000000..3e129bc --- /dev/null +++ b/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2022 Lukas Eisenhauer + * + * This program 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, either version 3 of the License, or(at your option) any later version. + * + * This program 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 this program. If not, see . + */ + +package com.lukas.music.ui.adapters + +import androidx.fragment.app.FragmentManager +import androidx.recyclerview.widget.RecyclerView +import com.lukas.music.R +import com.lukas.music.databinding.FragmentInstrumentBinding +import com.lukas.music.instruments.Instrument +import com.lukas.music.song.Song +import com.lukas.music.ui.fragments.EditInstrumentFragment +import com.lukas.music.util.setupToggle +import com.lukas.music.util.updateToggle + +class InstrumentViewHolder( + val binding: FragmentInstrumentBinding, + private val childFragmentManager: FragmentManager, + private val adapter: InstrumentAdapter +) : + RecyclerView.ViewHolder(binding.root) { + init { + HOLDERS += this + } + + var solo: Boolean = false + set(value) { + if (value) { + for (holder in HOLDERS) { + holder.solo = false + } + Song.currentSong.soloInstrument = instrument + } + field = value + binding.soloButton.updateToggle(this::solo, R.color.blue) + } + + var instrument: Instrument? = null + set(value) { + field = value + value ?: return + binding.instrumentNameText.text = instrument?.name + binding.editInstrumentButton.setOnClickListener { + EditInstrumentFragment(instrument!!, this).showNow(childFragmentManager, "") + } + binding.muteButton.setupToggle(instrument!!::muted, R.color.red) + binding.soloButton.setupToggle(this::solo, R.color.blue) { + if (!it) { + Song.currentSong.soloInstrument = null + } + } + binding.deleteButton.setOnClickListener { + val index = Instrument.instruments.indexOf(instrument) + Instrument.instruments -= instrument!! + adapter.notifyItemRemoved(index) + instrument!!.destroy() + } + } + + companion object { + val HOLDERS = mutableListOf() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/fragments/EditChordFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/EditChordFragment.kt index a311a6c..bd7a3d9 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/EditChordFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/EditChordFragment.kt @@ -10,13 +10,10 @@ package com.lukas.music.ui.fragments -import android.R import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.AdapterView -import android.widget.ArrayAdapter import androidx.fragment.app.DialogFragment import com.lukas.music.databinding.FragmentEditChordBinding import com.lukas.music.song.Scale @@ -24,8 +21,10 @@ import com.lukas.music.song.chords.Chord import com.lukas.music.song.chords.ChordType import com.lukas.music.song.chords.Interval +import com.lukas.music.util.setup -class EditChordFragment(val chord: Chord, val songFragment: SongFragment) : DialogFragment() { +class EditChordFragment(val chord: Chord, private val songFragment: SongFragment) : + DialogFragment() { lateinit var binding: FragmentEditChordBinding override fun onCreateView( @@ -45,24 +44,12 @@ val pitches = if (songFragment.displayChordNames) { Array(Scale.MAJOR.steps.size) { (Song.currentSong.root + Scale.MAJOR.steps[it]).noteName.toString() } } else Interval.IntervalName.NAMES - val pitchAdapter = ArrayAdapter( - binding.root.context, - R.layout.simple_spinner_dropdown_item, pitches - ) - binding.pitchSpinner.adapter = pitchAdapter - binding.pitchSpinner.setSelection(chord.interval.name.ordinal) - binding.pitchSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { - override fun onItemSelected( - parent: AdapterView<*>?, - view: View?, - position: Int, - id: Long - ) { - chord.note = Scale.MAJOR.steps[position] - songFragment.updateChords() + binding.pitchSpinner.setup(pitches, chord.interval.name.ordinal) { + chord.note = Scale.MAJOR.steps[it] + if (binding.typeSpinner.selectedItemPosition == 0) { + chord.chordType = Scale.MAJOR.chordTypes[chord.interval.name.ordinal] } - - override fun onNothingSelected(parent: AdapterView<*>?) {} + songFragment.updateChords() } } @@ -71,32 +58,17 @@ for (chordType in ChordType.VALUES) { values += chordType.toString() } - val modifierAdapter = ArrayAdapter( - binding.root.context, - R.layout.simple_spinner_dropdown_item, values - ) - binding.typeSpinner.adapter = modifierAdapter - binding.typeSpinner.setSelection( - if (chord.chordType == Scale.MAJOR.chordTypes[chord.interval.name.ordinal]) - 0 + binding.typeSpinner.setup( + values, + if (chord.chordType == Scale.MAJOR.chordTypes[chord.interval.name.ordinal]) 0 else chord.chordType.ordinal + 1 - ) - binding.typeSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { - override fun onItemSelected( - parent: AdapterView<*>?, - view: View?, - position: Int, - id: Long - ) { - if (position == 0) { - chord.chordType = Scale.MAJOR.chordTypes[chord.interval.name.ordinal] - } else { - chord.chordType = ChordType.VALUES[position - 1] - } - songFragment.updateChords() + ) { + if (it == 0) { + chord.chordType = Scale.MAJOR.chordTypes[chord.interval.name.ordinal] + } else { + chord.chordType = ChordType.VALUES[it - 1] } - - override fun onNothingSelected(parent: AdapterView<*>?) {} + songFragment.updateChords() } } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/fragments/EditInstrumentFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/EditInstrumentFragment.kt new file mode 100644 index 0000000..0020ae8 --- /dev/null +++ b/app/src/main/java/com/lukas/music/ui/fragments/EditInstrumentFragment.kt @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2022 Lukas Eisenhauer + * + * This program 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, either version 3 of the License, or(at your option) any later version. + * + * This program 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 this program. If not, see . + */ + +package com.lukas.music.ui.fragments + +import android.os.Bundle +import android.text.Editable +import android.text.TextWatcher +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.DialogFragment +import com.lukas.music.EditVoiceFragment +import com.lukas.music.databinding.FragmentEditInstrumentBinding +import com.lukas.music.instruments.Instrument +import com.lukas.music.instruments.Waveform +import com.lukas.music.song.voice.Voice +import com.lukas.music.ui.adapters.InstrumentViewHolder +import com.lukas.music.util.setup +import com.lukas.music.util.smartSetup + +class EditInstrumentFragment( + private val instrument: Instrument, + private val viewHolder: InstrumentViewHolder +) : DialogFragment() { + lateinit var binding: FragmentEditInstrumentBinding + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEditInstrumentBinding.inflate(inflater) + binding.instrumentNameTextBox.text.clear() + binding.instrumentNameTextBox.text.append(instrument.name) + binding.instrumentNameTextBox.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} + + override fun afterTextChanged(s: Editable?) { + instrument.name = binding.instrumentNameTextBox.text.toString() + viewHolder.instrument = viewHolder.instrument + } + }) + binding.waveformSelection.smartSetup(Waveform.VALUES, instrument::waveform) + binding.volumeSeek.setup(0, 100, 30) { + binding.volumeText.text = "volume: $it%" + instrument.volume = it.toFloat() / 100f + } + binding.voiceSelection.setup( + Voice.DEFAULT_VOICE_NAMES, + Voice.DEFAULT_VOICES.indexOf(instrument.voice::class) + ) { + if (instrument.voice::class == Voice.DEFAULT_VOICES[it]) { + return@setup + } + instrument.voice = Voice.DEFAULT_VOICES[it].constructors.first().call(instrument) + } + binding.editVoiceButton.setOnClickListener { + EditVoiceFragment(instrument.voice).showNow(childFragmentManager, "") + } + binding.closeButton.setOnClickListener { + dismiss() + } + return binding.root + } + + override fun onStart() { + super.onStart() + dialog?.window?.setLayout( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/fragments/InstrumentListFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/InstrumentListFragment.kt index 3f1a910..1d35c10 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/InstrumentListFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/InstrumentListFragment.kt @@ -10,13 +10,19 @@ package com.lukas.music.ui.fragments +import android.app.AlertDialog import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment +import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView import com.lukas.music.databinding.FragmentInstrumentListBinding +import com.lukas.music.instruments.Instrument +import com.lukas.music.instruments.MonoInstrument +import com.lukas.music.instruments.PolyInstrument import com.lukas.music.ui.adapters.InstrumentAdapter class InstrumentListFragment : Fragment() { @@ -27,8 +33,53 @@ savedInstanceState: Bundle? ): View? { binding = FragmentInstrumentListBinding.inflate(inflater) - binding.recyclerView.adapter = InstrumentAdapter() + binding.recyclerView.adapter = InstrumentAdapter(this) binding.recyclerView.layoutManager = LinearLayoutManager(context) + val callback = object : ItemTouchHelper.SimpleCallback( + ItemTouchHelper.UP or ItemTouchHelper.DOWN, + 0 + ) { + override fun onMove( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder + ): Boolean { + val adapter = recyclerView.adapter as InstrumentAdapter + val startPosition = viewHolder.adapterPosition + val endPosition = target.adapterPosition + val instrument = Instrument.instruments[startPosition] + Instrument.instruments.removeAt(startPosition) + if (endPosition < startPosition) { + Instrument.instruments.add(endPosition + 1, instrument) + } else { + Instrument.instruments.add(endPosition - 1, instrument) + } + adapter.notifyItemMoved(startPosition, endPosition) + return true + } + + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {} + } + val helper = ItemTouchHelper(callback) + helper.attachToRecyclerView(binding.recyclerView) + + val builder = AlertDialog.Builder(binding.root.context) + builder.setTitle("Instrument type:") + .setItems( + arrayOf("mono", "poly") + ) { _, index -> + when (index) { + 0 -> Instrument.instruments += MonoInstrument("New mono Instrument") + 1 -> Instrument.instruments += PolyInstrument("New poly Instrument") + } + (binding.recyclerView.adapter as RecyclerView.Adapter).notifyItemInserted( + Instrument.instruments.size - 1 + ) + } + builder.create() + binding.addInstrumentButton.setOnClickListener { + builder.show() + } return binding.root } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt index eaf2c61..0c33019 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt @@ -16,7 +16,10 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.* +import android.widget.RadioButton +import android.widget.RadioGroup +import android.widget.Space +import android.widget.TextView import androidx.cardview.widget.CardView import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment @@ -24,6 +27,7 @@ import com.lukas.music.databinding.FragmentPlayBinding import com.lukas.music.instruments.Rhythm import com.lukas.music.song.Song +import com.lukas.music.util.setup class PlayFragment : Fragment() { private lateinit var binding: FragmentPlayBinding @@ -58,11 +62,11 @@ } Song.currentSong.chordProgression.reverse() } - setupSlider(binding.masterVolumeSlider, 0, 100, 100) { + binding.masterVolumeSlider.setup(0, 100, 100) { setMasterVolume(it.toDouble() / 100.0) binding.masterVolumeText.text = "Master volume: $it%" } - setupSlider(binding.tempoSlider, 50, 150, 90) { + binding.tempoSlider.setup(50, 150, 90) { Rhythm.setTempo(it) binding.tempoText.text = "tempo: ${it}bpm" } @@ -116,33 +120,6 @@ ) } - private fun setupSlider( - slider: SeekBar, - min: Int, - max: Int, - initialProgress: Int, - callback: (Int) -> Unit - ) { - slider.min = min - slider.max = max - slider.setOnSeekBarChangeListener(object : - SeekBar.OnSeekBarChangeListener { - override fun onProgressChanged( - seekBar: SeekBar, - progress: Int, fromUser: Boolean - ) { - callback(progress) - } - - override fun onStartTrackingTouch(seekBar: SeekBar) { - } - - override fun onStopTrackingTouch(seekBar: SeekBar) { - } - }) - slider.progress = initialProgress - } - fun updateChords() { binding.phraseDisplay.removeAllViews() chordDisplays.clear() diff --git a/app/src/main/java/com/lukas/music/util/ArrayProperty.kt b/app/src/main/java/com/lukas/music/util/ArrayProperty.kt new file mode 100644 index 0000000..938f07c --- /dev/null +++ b/app/src/main/java/com/lukas/music/util/ArrayProperty.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2022 Lukas Eisenhauer + * + * This program 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, either version 3 of the License, or(at your option) any later version. + * + * This program 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 this program. If not, see . + */ + +package com.lukas.music.util + +import kotlin.reflect.* + +class ArrayProperty(private val array: Array, private val position: Int) : + KMutableProperty0 { + override val annotations: List = listOf() + override val isAbstract: Boolean = false + override val isFinal: Boolean = false + override val isOpen: Boolean = false + override val isSuspend: Boolean = false + override val name: String = "TODO" + override val parameters: List = listOf() + override val returnType: KType get() = TODO() + override val typeParameters: List = listOf() + override val visibility: KVisibility? = KVisibility.PUBLIC + override fun call(vararg args: Any?): T = TODO() + override fun callBy(args: Map): T = TODO() + override val setter: KMutableProperty0.Setter get() = TODO() + + + override val isConst: Boolean = false + override val isLateinit: Boolean = false + override val getter: KProperty0.Getter get() = TODO() + + + override fun getDelegate(): Any? = null + override fun invoke(): T = TODO() + + override fun set(value: T) { + array[position] = value + } + + + override fun get(): T = array[position] +} diff --git a/app/src/main/java/com/lukas/music/util/UIUtil.kt b/app/src/main/java/com/lukas/music/util/UIUtil.kt new file mode 100644 index 0000000..da897c5 --- /dev/null +++ b/app/src/main/java/com/lukas/music/util/UIUtil.kt @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2022 Lukas Eisenhauer + * + * This program 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, either version 3 of the License, or(at your option) any later version. + * + * This program 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 this program. If not, see . + */ + +package com.lukas.music.util + +import android.view.View +import android.widget.* +import androidx.core.content.ContextCompat +import com.lukas.music.R +import kotlin.reflect.KMutableProperty0 + +fun SeekBar.setup( + min: Int, max: Int, initialProgress: Int, callback: (Int) -> Unit +) { + this.min = min + this.max = max + setOnSeekBarChangeListener(object : + SeekBar.OnSeekBarChangeListener { + override fun onProgressChanged( + seekBar: SeekBar, + progress: Int, fromUser: Boolean + ) { + callback(progress) + } + + override fun onStartTrackingTouch(seekBar: SeekBar) { + } + + override fun onStopTrackingTouch(seekBar: SeekBar) { + } + }) + this.progress = initialProgress +} + +fun Button.setupToggle( + target: KMutableProperty0, + activeColor: Int, + callback: (Boolean) -> Unit = {} +) { + setOnClickListener { + target.set(!target.get()) + updateToggle(target, activeColor) + callback(target.get()) + } + updateToggle(target, activeColor) +} + +fun Button.updateToggle(target: KMutableProperty0, activeColor: Int) { + setBackgroundColor( + ContextCompat.getColor(context, if (target.get()) activeColor else R.color.gray_0x60) + ) +} + +fun Spinner.setup( + items: Array, + initialIndex: Int, + callback: (Int) -> Unit = {}, +) { + val arrayAdapter = ArrayAdapter( + context, + android.R.layout.simple_spinner_dropdown_item, items + ) + spinnerSetupMain(arrayAdapter, initialIndex, callback) +} + +fun Spinner.setup( + items: List, + initialIndex: Int, + callback: (Int) -> Unit = {}, +) { + val arrayAdapter = ArrayAdapter( + context, + android.R.layout.simple_spinner_dropdown_item, items + ) + spinnerSetupMain(arrayAdapter, initialIndex, callback) +} + +private fun Spinner.spinnerSetupMain( + arrayAdapter: ArrayAdapter, + initialIndex: Int, + callback: (Int) -> Unit +) { + adapter = arrayAdapter + setSelection(initialIndex) + onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onItemSelected( + parent: AdapterView<*>?, + view: View?, + position: Int, + id: Long + ) { + callback(position) + } + + override fun onNothingSelected(parent: AdapterView<*>?) {} + } +} + +fun > Spinner.smartSetup( + items: Array, + target: KMutableProperty0, + callback: (Int) -> Unit = {} +) { + val arrayAdapter = ArrayAdapter( + context, + android.R.layout.simple_spinner_dropdown_item, items + ) + spinnerSetupMain(arrayAdapter, target.get().ordinal) { + if (target.get() != items[it]) { + target.set(items[it]) + } + callback(it) + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_edit_instrument.xml b/app/src/main/res/layout/fragment_edit_instrument.xml new file mode 100644 index 0000000..b5578fc --- /dev/null +++ b/app/src/main/res/layout/fragment_edit_instrument.xml @@ -0,0 +1,151 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 97f8bf5..3d886c4 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,6 +11,8 @@ + + diff --git a/app/build.gradle b/app/build.gradle index 35173c4..97ec2a0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -56,12 +56,12 @@ } dependencies { - implementation 'androidx.core:core-ktx:1.7.0' implementation 'androidx.appcompat:appcompat:1.3.0' implementation 'com.google.android.material:material:1.6.1' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'androidx.gridlayout:gridlayout:1.0.0' + implementation 'org.jetbrains.kotlin:kotlin-reflect:1.7.20-Beta' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index dc80bd1..8e98b44 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -15,6 +15,9 @@ } AudioHost *thiz = static_cast(userData); for (auto const &instrument: *thiz->instruments) { + if (!instrument) { + continue; + } instrument->render(buffer, sampleCount); } for (uint32_t i = 0; i < sampleCount; i++) { diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 6ebd2b0..5505a35 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -8,7 +8,7 @@ static AudioHost *audioHost; -template +template void *listGet(_InputIterator iterator, uint32_t n) { for (uint32_t i = 0; i < n; i++) { iterator++; @@ -16,6 +16,18 @@ return *iterator; } +Instrument *getInstrument(uint32_t id) { + return static_cast(listGet(audioHost->instruments->begin(), id)); +} + +template +void listSet(_InputIterator iterator, uint32_t n, void *value) { + for (uint32_t i = 0; i < n; i++) { + iterator++; + } + *iterator = static_cast(value); +} + extern "C" { JNIEXPORT void JNICALL @@ -23,7 +35,6 @@ audioHost = new AudioHost(); } -extern "C" JNIEXPORT jint JNICALL Java_com_lukas_music_instruments_InternalInstrument_createInstrument(JNIEnv *env, jobject thiz) { uint32_t result = audioHost->instruments->size(); @@ -32,43 +43,37 @@ return result; } -extern "C" -JNIEXPORT void JNICALL -Java_com_lukas_music_instruments_InternalInstrument_setInstrumentActive(JNIEnv *env, jobject thiz, - jint id, jboolean active) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->wave->amplitude = active ? 0.3 : 0.0; -} -} -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_startNote(JNIEnv *env, jobject thiz, jint id, jdouble frequency) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->startNote(frequency); + getInstrument(id)->startNote(frequency); } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_endNote(JNIEnv *env, jobject thiz, jint id) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->endNote(); + getInstrument(id)->endNote(); } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_ui_fragments_PlayFragment_setMasterVolume(JNIEnv *env, jobject thiz, jdouble volume) { audioHost->masterVolume = volume; } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setInstrumentWaveform(JNIEnv *env, jobject thiz, jint id, jint waveform) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->setWaveform(static_cast(waveform)); + getInstrument(id)->setWaveform(static_cast(waveform)); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, + jfloat volume) { + getInstrument(id)->wave->amplitude = volume; +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_destroy(JNIEnv *env, jobject thiz, jint id) { + listSet(audioHost->instruments->begin(), id, nullptr); + delete getInstrument(id); +} } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt new file mode 100644 index 0000000..56f21e6 --- /dev/null +++ b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2022 Lukas Eisenhauer + * + * This program 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, either version 3 of the License, or(at your option) any later version. + * + * This program 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 this program. If not, see . + */ + +package com.lukas.music + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TableRow +import androidx.core.view.setMargins +import androidx.fragment.app.DialogFragment +import com.google.android.material.button.MaterialButton +import com.lukas.music.databinding.FragmentEditVoiceBinding +import com.lukas.music.song.Song +import com.lukas.music.song.voice.Voice +import com.lukas.music.util.ArrayProperty +import com.lukas.music.util.setupToggle + +class EditVoiceFragment(private val voice: Voice) : DialogFragment() { + private lateinit var binding: FragmentEditVoiceBinding + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEditVoiceBinding.inflate(inflater) + for (row in 0 until voice.noteCount) { + val rowLayout = TableRow(binding.root.context) + for (column in 0 until Song.currentSong.beats) { + val button = MaterialButton(binding.root.context) + button.layoutParams = buttonLayout + button.setupToggle(ArrayProperty(voice.noteActive[column], row), R.color.blue) + rowLayout.addView(button) + } + binding.noteGrid.addView(rowLayout) + } + binding.noteGrid.isStretchAllColumns = true + binding.closeButton.setOnClickListener { + dismiss() + } + return binding.root + } + + override fun onStart() { + super.onStart() + dialog?.window?.setLayout( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + } + + companion object { + val buttonLayout = TableRow.LayoutParams(0, TableRow.LayoutParams.WRAP_CONTENT) + + init { + buttonLayout.setMargins(5) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Instrument.kt b/app/src/main/java/com/lukas/music/instruments/Instrument.kt index 8ad90c6..92a896c 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -10,42 +10,22 @@ package com.lukas.music.instruments -import com.lukas.music.databinding.FragmentInstrumentBinding import com.lukas.music.song.note.Note import com.lukas.music.song.voice.BassVoice -import com.lukas.music.song.voice.ChordVoice import com.lukas.music.song.voice.Voice -abstract class Instrument(private var name: String) { - private var active = false +abstract class Instrument(var name: String) { + var voice: Voice = BassVoice(this) abstract var waveform: Waveform - - fun applyToView(binding: FragmentInstrumentBinding) { - binding.instrumentNameText.text = name - binding.editInstrumentButton.setOnClickListener { - println("click instrument $name") - } - binding.activeSwitch.setOnCheckedChangeListener { _, newActive -> - active = newActive - changeActive(newActive) - } - binding.activeSwitch.isChecked = active - } + abstract var volume: Float + abstract var muted: Boolean abstract fun startNote(note: Note) abstract fun stop() - abstract fun changeActive(newActive: Boolean) + abstract fun stopNote(note: Note) + abstract fun destroy() companion object { - val instruments = - mutableListOf( - MonoInstrument("Bass"), - PolyInstrument("Chords"), - ) - - val voice = mutableListOf( - BassVoice(instruments[0]), - ChordVoice(instruments[1]), - ) + val instruments = mutableListOf() } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt index dc64bcc..b4c68a4 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -10,33 +10,67 @@ package com.lukas.music.instruments +import com.lukas.music.song.note.Note + class InternalInstrument { private val id = createInstrument() - var active: Boolean = false - set(value) { - field = value - setInstrumentActive(id, value) - } + var note: Note? = null var waveform: Waveform = Waveform.SINE set(value) { field = value setInstrumentWaveform(id, value.id) - // this is to resend the setInstrumentActive for the new waveform in the internal c++ code - active = active + refresh() } - fun startNote(frequency: Double) { - startNote(id, frequency) + var volume: Float = 0.3f + set(value) { + field = value + if (!muted) { + actualVolume = value + } + } + + var muted: Boolean = false + set(value) { + field = value + actualVolume = if (value) 0.0f else volume + } + + private var actualVolume: Float = 1.0f + set(value) { + field = value + setVolume(id, value) + } + + init { + refresh() + } + + private fun refresh() { + // this is to resend the old information to the internal c++ code (when changing the waveform) + muted = muted + volume = volume + } + + fun startNote(note: Note) { + this.note = note + startNote(id, note.frequency) } fun endNote() { + note = null endNote(id) } + fun destroy() { + destroy(id) + } + private external fun createInstrument(): Int - private external fun setInstrumentActive(id: Int, isActive: Boolean) private external fun setInstrumentWaveform(id: Int, waveform: Int) private external fun startNote(id: Int, frequency: Double) private external fun endNote(id: Int) + private external fun setVolume(id: Int, volume: Float) + private external fun destroy(id: Int) } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt index e7b7464..2b43524 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -21,15 +21,36 @@ internalInstrument.waveform = value } - override fun startNote(note: Note) { - internalInstrument.startNote(note.frequency) - } + override var volume: Float = 1.0f + set(value) { + field = value + internalInstrument.volume = volume + } - override fun changeActive(newActive: Boolean) { - internalInstrument.active = newActive + override var muted: Boolean = false + set(value) { + field = value + internalInstrument.muted = value + } + + override fun startNote(note: Note) { + if (note == internalInstrument.note) { + return + } + internalInstrument.startNote(note) } override fun stop() { internalInstrument.endNote() } + + override fun stopNote(note: Note) { + if (note == internalInstrument.note) { + stop() + } + } + + override fun destroy() { + internalInstrument.destroy() + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt index 9d8b4f9..26e4fbb 100644 --- a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt @@ -24,27 +24,55 @@ } } + override var volume: Float = 1.0f + set(value) { + field = value + for (internalInstrument in internalInstruments) { + internalInstrument.volume = volume + } + } + + override var muted: Boolean = false + set(value) { + field = value + for (instrument in internalInstruments) { + instrument.muted = value + } + } + override fun startNote(note: Note) { for ((index, instrumentPlaying) in playing.withIndex()) { if (!instrumentPlaying) { - internalInstruments[index].startNote(note.frequency) + internalInstruments[index].startNote(note) playing[index] = true return } + if (internalInstruments[index].note == note) { + return + } } throw IllegalStateException("cannot start another note with the current amount of oscillators") } - override fun changeActive(newActive: Boolean) { - for (instrument in internalInstruments) { - instrument.active = newActive - } - } - override fun stop() { for ((i, instrument) in internalInstruments.withIndex()) { instrument.endNote() playing[i] = false } } + + override fun stopNote(note: Note) { + for ((i, instrument) in internalInstruments.withIndex()) { + if (instrument.note == note) { + instrument.endNote() + playing[i] = false + } + } + } + + override fun destroy() { + for (instrument in internalInstruments) { + instrument.destroy() + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/Song.kt b/app/src/main/java/com/lukas/music/song/Song.kt index a3123f2..fc5421b 100644 --- a/app/src/main/java/com/lukas/music/song/Song.kt +++ b/app/src/main/java/com/lukas/music/song/Song.kt @@ -16,10 +16,33 @@ import com.lukas.music.util.Cycle class Song( - var root: Note, + root: Note, val beats: Int ) : Cycle(beats) { val chordProgression = ChordProgression() + var soloInstrument: Instrument? = null + set(value) { + field = value + value?.let { + for (instrument in Instrument.instruments) { + if (instrument != value) { + instrument.stop() + } + } + } + } + + var root: Note = root + set(value) { + field = value + stopAllInstruments() + } + + private fun stopAllInstruments() { + for (instrument in Instrument.instruments) { + instrument.stop() + } + } init { for (i in 0 until beats) { @@ -27,6 +50,7 @@ } wraparoundListeners += { chordProgression.step() + stopAllInstruments() } } @@ -34,8 +58,12 @@ super.step() val chord = chordProgression.currentItem?.currentItem ?: return index val chordNotes = chord.getNotes(root) - for (voice in Instrument.voice) { - voice.step(root, chordNotes) + soloInstrument?.let { + it.voice.step(root, chordNotes, index) + } ?: run { + for (instrument in Instrument.instruments) { + instrument.voice.step(root, chordNotes, index) + } } return index } diff --git a/app/src/main/java/com/lukas/music/song/chords/Chord.kt b/app/src/main/java/com/lukas/music/song/chords/Chord.kt index db45fdd..4400dae 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Chord.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Chord.kt @@ -19,6 +19,12 @@ interval = Interval(value) } var interval = Interval(note) + set(value) { + field = value + if (note != value.distance) { + note = value.distance + } + } fun getNotes(root: Note): Array { return Array(chordType.notes.size) { root + note + chordType.notes[it] } diff --git a/app/src/main/java/com/lukas/music/song/chords/Interval.kt b/app/src/main/java/com/lukas/music/song/chords/Interval.kt index 2726787..d112805 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Interval.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Interval.kt @@ -10,7 +10,7 @@ package com.lukas.music.song.chords -class Interval(private val distance: Int) { +class Interval(val distance: Int) { val name: IntervalName = when (distance) { 0 -> IntervalName.UNISON 1, 2 -> IntervalName.SECOND @@ -32,7 +32,7 @@ return name.toString() } - enum class IntervalName(val distance: Int, val romanVersion: String) { + enum class IntervalName(private val distance: Int, val romanVersion: String) { UNISON(0, "I"), SECOND(1, "II"), THIRD(3, "III"), diff --git a/app/src/main/java/com/lukas/music/song/note/Note.kt b/app/src/main/java/com/lukas/music/song/note/Note.kt index d3a588d..17bfb2b 100644 --- a/app/src/main/java/com/lukas/music/song/note/Note.kt +++ b/app/src/main/java/com/lukas/music/song/note/Note.kt @@ -28,6 +28,22 @@ return this + (-other) } + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Note + + if (id != other.id) return false + + return true + } + + override fun hashCode(): Int { + return id + } + + companion object { val NOTES = Array(128) { Note(it) } diff --git a/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt b/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt index 3990554..4706068 100644 --- a/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt +++ b/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt @@ -14,9 +14,16 @@ import com.lukas.music.song.note.Note class BassVoice(instrument: Instrument) : Voice(instrument) { - override val steps = listOf(1, 3) + override var noteActive: Array> = arrayOf( + arrayOf(true), + arrayOf(false), + arrayOf(true), + arrayOf(false) + ) - override fun step(root: Note, chord: Array) { - instrument.startNote(chord[0] - 24) + override val noteCount: Int = 1 + + override fun getNotes(root: Note, chordNotes: Array): Array { + return arrayOf(chordNotes[0] - 24) } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/voice/ChordVoice.kt b/app/src/main/java/com/lukas/music/song/voice/ChordVoice.kt index fc5362d..ab7117f 100644 --- a/app/src/main/java/com/lukas/music/song/voice/ChordVoice.kt +++ b/app/src/main/java/com/lukas/music/song/voice/ChordVoice.kt @@ -14,12 +14,15 @@ import com.lukas.music.song.note.Note class ChordVoice(instrument: Instrument) : Voice(instrument) { - override val steps: List = listOf(2, 4) + override var noteActive: Array> = arrayOf( + Array(3) { false }, + Array(3) { true }, + Array(3) { false }, + Array(3) { true }, + ) + override val noteCount: Int = 3 - override fun step(root: Note, chord: Array) { - instrument.stop() - for (note in chord) { - instrument.startNote(note) - } + override fun getNotes(root: Note, chordNotes: Array): Array { + return chordNotes } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/voice/Voice.kt b/app/src/main/java/com/lukas/music/song/voice/Voice.kt index f7fca94..4f56c2a 100644 --- a/app/src/main/java/com/lukas/music/song/voice/Voice.kt +++ b/app/src/main/java/com/lukas/music/song/voice/Voice.kt @@ -12,9 +12,36 @@ import com.lukas.music.instruments.Instrument import com.lukas.music.song.note.Note +import kotlin.reflect.KClass abstract class Voice(val instrument: Instrument) { - abstract val steps: List + abstract var noteActive: Array> + abstract val noteCount: Int - abstract fun step(root: Note, chord: Array) + abstract fun getNotes(root: Note, chordNotes: Array): Array + + fun step(root: Note, chordNotes: Array, beat: Int) { + if (instrument.muted) { + return + } + val activeNotes = noteActive[beat] + val notes = getNotes(root, chordNotes) + for ((index, active) in activeNotes.withIndex()) { + val note = notes[index] + if (!active) { + instrument.stopNote(note) + continue + } + instrument.startNote(note) + } + } + + companion object { + val DEFAULT_VOICES = listOf>( + BassVoice::class, + ChordVoice::class, + ) + + val DEFAULT_VOICE_NAMES = listOf("Bass", "Chord") + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/adapters/InstrumentAdapter.kt b/app/src/main/java/com/lukas/music/ui/adapters/InstrumentAdapter.kt index 8961831..d51f62e 100644 --- a/app/src/main/java/com/lukas/music/ui/adapters/InstrumentAdapter.kt +++ b/app/src/main/java/com/lukas/music/ui/adapters/InstrumentAdapter.kt @@ -11,54 +11,26 @@ package com.lukas.music.ui.adapters import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup -import android.widget.AdapterView -import android.widget.ArrayAdapter import androidx.recyclerview.widget.RecyclerView import com.lukas.music.databinding.FragmentInstrumentBinding import com.lukas.music.instruments.Instrument -import com.lukas.music.instruments.Waveform +import com.lukas.music.ui.fragments.InstrumentListFragment -class InstrumentAdapter : RecyclerView.Adapter() { - class InstrumentViewHolder(val binding: FragmentInstrumentBinding) : - RecyclerView.ViewHolder(binding.root), AdapterView.OnItemSelectedListener { - lateinit var instrument: Instrument - - init { - val adapter = ArrayAdapter( - binding.root.context, - android.R.layout.simple_spinner_dropdown_item, Waveform.VALUES - ) - binding.waveformSelection.adapter = adapter - binding.waveformSelection.onItemSelectedListener = this - } - - override fun onItemSelected( - adapterView: AdapterView<*>?, - view: View?, - position: Int, - id: Long - ) { - instrument.waveform = Waveform.VALUES[position] - } - - override fun onNothingSelected(adapterView: AdapterView<*>?) { - } - } +class InstrumentAdapter(val parent: InstrumentListFragment) : + RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): InstrumentViewHolder { val context = parent.context val inflater = LayoutInflater.from(context) val binding = FragmentInstrumentBinding.inflate(inflater, parent, false) - return InstrumentViewHolder(binding) + return InstrumentViewHolder(binding, this.parent.childFragmentManager, this) } override fun onBindViewHolder(holder: InstrumentViewHolder, position: Int) { val instrument = Instrument.instruments[position] holder.instrument = instrument - instrument.applyToView(holder.binding) } override fun getItemCount(): Int { diff --git a/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt b/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt new file mode 100644 index 0000000..3e129bc --- /dev/null +++ b/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2022 Lukas Eisenhauer + * + * This program 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, either version 3 of the License, or(at your option) any later version. + * + * This program 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 this program. If not, see . + */ + +package com.lukas.music.ui.adapters + +import androidx.fragment.app.FragmentManager +import androidx.recyclerview.widget.RecyclerView +import com.lukas.music.R +import com.lukas.music.databinding.FragmentInstrumentBinding +import com.lukas.music.instruments.Instrument +import com.lukas.music.song.Song +import com.lukas.music.ui.fragments.EditInstrumentFragment +import com.lukas.music.util.setupToggle +import com.lukas.music.util.updateToggle + +class InstrumentViewHolder( + val binding: FragmentInstrumentBinding, + private val childFragmentManager: FragmentManager, + private val adapter: InstrumentAdapter +) : + RecyclerView.ViewHolder(binding.root) { + init { + HOLDERS += this + } + + var solo: Boolean = false + set(value) { + if (value) { + for (holder in HOLDERS) { + holder.solo = false + } + Song.currentSong.soloInstrument = instrument + } + field = value + binding.soloButton.updateToggle(this::solo, R.color.blue) + } + + var instrument: Instrument? = null + set(value) { + field = value + value ?: return + binding.instrumentNameText.text = instrument?.name + binding.editInstrumentButton.setOnClickListener { + EditInstrumentFragment(instrument!!, this).showNow(childFragmentManager, "") + } + binding.muteButton.setupToggle(instrument!!::muted, R.color.red) + binding.soloButton.setupToggle(this::solo, R.color.blue) { + if (!it) { + Song.currentSong.soloInstrument = null + } + } + binding.deleteButton.setOnClickListener { + val index = Instrument.instruments.indexOf(instrument) + Instrument.instruments -= instrument!! + adapter.notifyItemRemoved(index) + instrument!!.destroy() + } + } + + companion object { + val HOLDERS = mutableListOf() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/fragments/EditChordFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/EditChordFragment.kt index a311a6c..bd7a3d9 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/EditChordFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/EditChordFragment.kt @@ -10,13 +10,10 @@ package com.lukas.music.ui.fragments -import android.R import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.AdapterView -import android.widget.ArrayAdapter import androidx.fragment.app.DialogFragment import com.lukas.music.databinding.FragmentEditChordBinding import com.lukas.music.song.Scale @@ -24,8 +21,10 @@ import com.lukas.music.song.chords.Chord import com.lukas.music.song.chords.ChordType import com.lukas.music.song.chords.Interval +import com.lukas.music.util.setup -class EditChordFragment(val chord: Chord, val songFragment: SongFragment) : DialogFragment() { +class EditChordFragment(val chord: Chord, private val songFragment: SongFragment) : + DialogFragment() { lateinit var binding: FragmentEditChordBinding override fun onCreateView( @@ -45,24 +44,12 @@ val pitches = if (songFragment.displayChordNames) { Array(Scale.MAJOR.steps.size) { (Song.currentSong.root + Scale.MAJOR.steps[it]).noteName.toString() } } else Interval.IntervalName.NAMES - val pitchAdapter = ArrayAdapter( - binding.root.context, - R.layout.simple_spinner_dropdown_item, pitches - ) - binding.pitchSpinner.adapter = pitchAdapter - binding.pitchSpinner.setSelection(chord.interval.name.ordinal) - binding.pitchSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { - override fun onItemSelected( - parent: AdapterView<*>?, - view: View?, - position: Int, - id: Long - ) { - chord.note = Scale.MAJOR.steps[position] - songFragment.updateChords() + binding.pitchSpinner.setup(pitches, chord.interval.name.ordinal) { + chord.note = Scale.MAJOR.steps[it] + if (binding.typeSpinner.selectedItemPosition == 0) { + chord.chordType = Scale.MAJOR.chordTypes[chord.interval.name.ordinal] } - - override fun onNothingSelected(parent: AdapterView<*>?) {} + songFragment.updateChords() } } @@ -71,32 +58,17 @@ for (chordType in ChordType.VALUES) { values += chordType.toString() } - val modifierAdapter = ArrayAdapter( - binding.root.context, - R.layout.simple_spinner_dropdown_item, values - ) - binding.typeSpinner.adapter = modifierAdapter - binding.typeSpinner.setSelection( - if (chord.chordType == Scale.MAJOR.chordTypes[chord.interval.name.ordinal]) - 0 + binding.typeSpinner.setup( + values, + if (chord.chordType == Scale.MAJOR.chordTypes[chord.interval.name.ordinal]) 0 else chord.chordType.ordinal + 1 - ) - binding.typeSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { - override fun onItemSelected( - parent: AdapterView<*>?, - view: View?, - position: Int, - id: Long - ) { - if (position == 0) { - chord.chordType = Scale.MAJOR.chordTypes[chord.interval.name.ordinal] - } else { - chord.chordType = ChordType.VALUES[position - 1] - } - songFragment.updateChords() + ) { + if (it == 0) { + chord.chordType = Scale.MAJOR.chordTypes[chord.interval.name.ordinal] + } else { + chord.chordType = ChordType.VALUES[it - 1] } - - override fun onNothingSelected(parent: AdapterView<*>?) {} + songFragment.updateChords() } } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/fragments/EditInstrumentFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/EditInstrumentFragment.kt new file mode 100644 index 0000000..0020ae8 --- /dev/null +++ b/app/src/main/java/com/lukas/music/ui/fragments/EditInstrumentFragment.kt @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2022 Lukas Eisenhauer + * + * This program 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, either version 3 of the License, or(at your option) any later version. + * + * This program 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 this program. If not, see . + */ + +package com.lukas.music.ui.fragments + +import android.os.Bundle +import android.text.Editable +import android.text.TextWatcher +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.DialogFragment +import com.lukas.music.EditVoiceFragment +import com.lukas.music.databinding.FragmentEditInstrumentBinding +import com.lukas.music.instruments.Instrument +import com.lukas.music.instruments.Waveform +import com.lukas.music.song.voice.Voice +import com.lukas.music.ui.adapters.InstrumentViewHolder +import com.lukas.music.util.setup +import com.lukas.music.util.smartSetup + +class EditInstrumentFragment( + private val instrument: Instrument, + private val viewHolder: InstrumentViewHolder +) : DialogFragment() { + lateinit var binding: FragmentEditInstrumentBinding + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEditInstrumentBinding.inflate(inflater) + binding.instrumentNameTextBox.text.clear() + binding.instrumentNameTextBox.text.append(instrument.name) + binding.instrumentNameTextBox.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} + + override fun afterTextChanged(s: Editable?) { + instrument.name = binding.instrumentNameTextBox.text.toString() + viewHolder.instrument = viewHolder.instrument + } + }) + binding.waveformSelection.smartSetup(Waveform.VALUES, instrument::waveform) + binding.volumeSeek.setup(0, 100, 30) { + binding.volumeText.text = "volume: $it%" + instrument.volume = it.toFloat() / 100f + } + binding.voiceSelection.setup( + Voice.DEFAULT_VOICE_NAMES, + Voice.DEFAULT_VOICES.indexOf(instrument.voice::class) + ) { + if (instrument.voice::class == Voice.DEFAULT_VOICES[it]) { + return@setup + } + instrument.voice = Voice.DEFAULT_VOICES[it].constructors.first().call(instrument) + } + binding.editVoiceButton.setOnClickListener { + EditVoiceFragment(instrument.voice).showNow(childFragmentManager, "") + } + binding.closeButton.setOnClickListener { + dismiss() + } + return binding.root + } + + override fun onStart() { + super.onStart() + dialog?.window?.setLayout( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/fragments/InstrumentListFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/InstrumentListFragment.kt index 3f1a910..1d35c10 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/InstrumentListFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/InstrumentListFragment.kt @@ -10,13 +10,19 @@ package com.lukas.music.ui.fragments +import android.app.AlertDialog import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment +import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView import com.lukas.music.databinding.FragmentInstrumentListBinding +import com.lukas.music.instruments.Instrument +import com.lukas.music.instruments.MonoInstrument +import com.lukas.music.instruments.PolyInstrument import com.lukas.music.ui.adapters.InstrumentAdapter class InstrumentListFragment : Fragment() { @@ -27,8 +33,53 @@ savedInstanceState: Bundle? ): View? { binding = FragmentInstrumentListBinding.inflate(inflater) - binding.recyclerView.adapter = InstrumentAdapter() + binding.recyclerView.adapter = InstrumentAdapter(this) binding.recyclerView.layoutManager = LinearLayoutManager(context) + val callback = object : ItemTouchHelper.SimpleCallback( + ItemTouchHelper.UP or ItemTouchHelper.DOWN, + 0 + ) { + override fun onMove( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder + ): Boolean { + val adapter = recyclerView.adapter as InstrumentAdapter + val startPosition = viewHolder.adapterPosition + val endPosition = target.adapterPosition + val instrument = Instrument.instruments[startPosition] + Instrument.instruments.removeAt(startPosition) + if (endPosition < startPosition) { + Instrument.instruments.add(endPosition + 1, instrument) + } else { + Instrument.instruments.add(endPosition - 1, instrument) + } + adapter.notifyItemMoved(startPosition, endPosition) + return true + } + + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {} + } + val helper = ItemTouchHelper(callback) + helper.attachToRecyclerView(binding.recyclerView) + + val builder = AlertDialog.Builder(binding.root.context) + builder.setTitle("Instrument type:") + .setItems( + arrayOf("mono", "poly") + ) { _, index -> + when (index) { + 0 -> Instrument.instruments += MonoInstrument("New mono Instrument") + 1 -> Instrument.instruments += PolyInstrument("New poly Instrument") + } + (binding.recyclerView.adapter as RecyclerView.Adapter).notifyItemInserted( + Instrument.instruments.size - 1 + ) + } + builder.create() + binding.addInstrumentButton.setOnClickListener { + builder.show() + } return binding.root } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt index eaf2c61..0c33019 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt @@ -16,7 +16,10 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.* +import android.widget.RadioButton +import android.widget.RadioGroup +import android.widget.Space +import android.widget.TextView import androidx.cardview.widget.CardView import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment @@ -24,6 +27,7 @@ import com.lukas.music.databinding.FragmentPlayBinding import com.lukas.music.instruments.Rhythm import com.lukas.music.song.Song +import com.lukas.music.util.setup class PlayFragment : Fragment() { private lateinit var binding: FragmentPlayBinding @@ -58,11 +62,11 @@ } Song.currentSong.chordProgression.reverse() } - setupSlider(binding.masterVolumeSlider, 0, 100, 100) { + binding.masterVolumeSlider.setup(0, 100, 100) { setMasterVolume(it.toDouble() / 100.0) binding.masterVolumeText.text = "Master volume: $it%" } - setupSlider(binding.tempoSlider, 50, 150, 90) { + binding.tempoSlider.setup(50, 150, 90) { Rhythm.setTempo(it) binding.tempoText.text = "tempo: ${it}bpm" } @@ -116,33 +120,6 @@ ) } - private fun setupSlider( - slider: SeekBar, - min: Int, - max: Int, - initialProgress: Int, - callback: (Int) -> Unit - ) { - slider.min = min - slider.max = max - slider.setOnSeekBarChangeListener(object : - SeekBar.OnSeekBarChangeListener { - override fun onProgressChanged( - seekBar: SeekBar, - progress: Int, fromUser: Boolean - ) { - callback(progress) - } - - override fun onStartTrackingTouch(seekBar: SeekBar) { - } - - override fun onStopTrackingTouch(seekBar: SeekBar) { - } - }) - slider.progress = initialProgress - } - fun updateChords() { binding.phraseDisplay.removeAllViews() chordDisplays.clear() diff --git a/app/src/main/java/com/lukas/music/util/ArrayProperty.kt b/app/src/main/java/com/lukas/music/util/ArrayProperty.kt new file mode 100644 index 0000000..938f07c --- /dev/null +++ b/app/src/main/java/com/lukas/music/util/ArrayProperty.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2022 Lukas Eisenhauer + * + * This program 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, either version 3 of the License, or(at your option) any later version. + * + * This program 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 this program. If not, see . + */ + +package com.lukas.music.util + +import kotlin.reflect.* + +class ArrayProperty(private val array: Array, private val position: Int) : + KMutableProperty0 { + override val annotations: List = listOf() + override val isAbstract: Boolean = false + override val isFinal: Boolean = false + override val isOpen: Boolean = false + override val isSuspend: Boolean = false + override val name: String = "TODO" + override val parameters: List = listOf() + override val returnType: KType get() = TODO() + override val typeParameters: List = listOf() + override val visibility: KVisibility? = KVisibility.PUBLIC + override fun call(vararg args: Any?): T = TODO() + override fun callBy(args: Map): T = TODO() + override val setter: KMutableProperty0.Setter get() = TODO() + + + override val isConst: Boolean = false + override val isLateinit: Boolean = false + override val getter: KProperty0.Getter get() = TODO() + + + override fun getDelegate(): Any? = null + override fun invoke(): T = TODO() + + override fun set(value: T) { + array[position] = value + } + + + override fun get(): T = array[position] +} diff --git a/app/src/main/java/com/lukas/music/util/UIUtil.kt b/app/src/main/java/com/lukas/music/util/UIUtil.kt new file mode 100644 index 0000000..da897c5 --- /dev/null +++ b/app/src/main/java/com/lukas/music/util/UIUtil.kt @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2022 Lukas Eisenhauer + * + * This program 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, either version 3 of the License, or(at your option) any later version. + * + * This program 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 this program. If not, see . + */ + +package com.lukas.music.util + +import android.view.View +import android.widget.* +import androidx.core.content.ContextCompat +import com.lukas.music.R +import kotlin.reflect.KMutableProperty0 + +fun SeekBar.setup( + min: Int, max: Int, initialProgress: Int, callback: (Int) -> Unit +) { + this.min = min + this.max = max + setOnSeekBarChangeListener(object : + SeekBar.OnSeekBarChangeListener { + override fun onProgressChanged( + seekBar: SeekBar, + progress: Int, fromUser: Boolean + ) { + callback(progress) + } + + override fun onStartTrackingTouch(seekBar: SeekBar) { + } + + override fun onStopTrackingTouch(seekBar: SeekBar) { + } + }) + this.progress = initialProgress +} + +fun Button.setupToggle( + target: KMutableProperty0, + activeColor: Int, + callback: (Boolean) -> Unit = {} +) { + setOnClickListener { + target.set(!target.get()) + updateToggle(target, activeColor) + callback(target.get()) + } + updateToggle(target, activeColor) +} + +fun Button.updateToggle(target: KMutableProperty0, activeColor: Int) { + setBackgroundColor( + ContextCompat.getColor(context, if (target.get()) activeColor else R.color.gray_0x60) + ) +} + +fun Spinner.setup( + items: Array, + initialIndex: Int, + callback: (Int) -> Unit = {}, +) { + val arrayAdapter = ArrayAdapter( + context, + android.R.layout.simple_spinner_dropdown_item, items + ) + spinnerSetupMain(arrayAdapter, initialIndex, callback) +} + +fun Spinner.setup( + items: List, + initialIndex: Int, + callback: (Int) -> Unit = {}, +) { + val arrayAdapter = ArrayAdapter( + context, + android.R.layout.simple_spinner_dropdown_item, items + ) + spinnerSetupMain(arrayAdapter, initialIndex, callback) +} + +private fun Spinner.spinnerSetupMain( + arrayAdapter: ArrayAdapter, + initialIndex: Int, + callback: (Int) -> Unit +) { + adapter = arrayAdapter + setSelection(initialIndex) + onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onItemSelected( + parent: AdapterView<*>?, + view: View?, + position: Int, + id: Long + ) { + callback(position) + } + + override fun onNothingSelected(parent: AdapterView<*>?) {} + } +} + +fun > Spinner.smartSetup( + items: Array, + target: KMutableProperty0, + callback: (Int) -> Unit = {} +) { + val arrayAdapter = ArrayAdapter( + context, + android.R.layout.simple_spinner_dropdown_item, items + ) + spinnerSetupMain(arrayAdapter, target.get().ordinal) { + if (target.get() != items[it]) { + target.set(items[it]) + } + callback(it) + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_edit_instrument.xml b/app/src/main/res/layout/fragment_edit_instrument.xml new file mode 100644 index 0000000..b5578fc --- /dev/null +++ b/app/src/main/res/layout/fragment_edit_instrument.xml @@ -0,0 +1,151 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_edit_voice.xml b/app/src/main/res/layout/fragment_edit_voice.xml new file mode 100644 index 0000000..b29b2c7 --- /dev/null +++ b/app/src/main/res/layout/fragment_edit_voice.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 97f8bf5..3d886c4 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,6 +11,8 @@ + + diff --git a/app/build.gradle b/app/build.gradle index 35173c4..97ec2a0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -56,12 +56,12 @@ } dependencies { - implementation 'androidx.core:core-ktx:1.7.0' implementation 'androidx.appcompat:appcompat:1.3.0' implementation 'com.google.android.material:material:1.6.1' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'androidx.gridlayout:gridlayout:1.0.0' + implementation 'org.jetbrains.kotlin:kotlin-reflect:1.7.20-Beta' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' diff --git a/app/src/main/cpp/AudioHost.cpp b/app/src/main/cpp/AudioHost.cpp index dc80bd1..8e98b44 100644 --- a/app/src/main/cpp/AudioHost.cpp +++ b/app/src/main/cpp/AudioHost.cpp @@ -15,6 +15,9 @@ } AudioHost *thiz = static_cast(userData); for (auto const &instrument: *thiz->instruments) { + if (!instrument) { + continue; + } instrument->render(buffer, sampleCount); } for (uint32_t i = 0; i < sampleCount; i++) { diff --git a/app/src/main/cpp/JavaFunctions.cpp b/app/src/main/cpp/JavaFunctions.cpp index 6ebd2b0..5505a35 100644 --- a/app/src/main/cpp/JavaFunctions.cpp +++ b/app/src/main/cpp/JavaFunctions.cpp @@ -8,7 +8,7 @@ static AudioHost *audioHost; -template +template void *listGet(_InputIterator iterator, uint32_t n) { for (uint32_t i = 0; i < n; i++) { iterator++; @@ -16,6 +16,18 @@ return *iterator; } +Instrument *getInstrument(uint32_t id) { + return static_cast(listGet(audioHost->instruments->begin(), id)); +} + +template +void listSet(_InputIterator iterator, uint32_t n, void *value) { + for (uint32_t i = 0; i < n; i++) { + iterator++; + } + *iterator = static_cast(value); +} + extern "C" { JNIEXPORT void JNICALL @@ -23,7 +35,6 @@ audioHost = new AudioHost(); } -extern "C" JNIEXPORT jint JNICALL Java_com_lukas_music_instruments_InternalInstrument_createInstrument(JNIEnv *env, jobject thiz) { uint32_t result = audioHost->instruments->size(); @@ -32,43 +43,37 @@ return result; } -extern "C" -JNIEXPORT void JNICALL -Java_com_lukas_music_instruments_InternalInstrument_setInstrumentActive(JNIEnv *env, jobject thiz, - jint id, jboolean active) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->wave->amplitude = active ? 0.3 : 0.0; -} -} -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_startNote(JNIEnv *env, jobject thiz, jint id, jdouble frequency) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->startNote(frequency); + getInstrument(id)->startNote(frequency); } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_endNote(JNIEnv *env, jobject thiz, jint id) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->endNote(); + getInstrument(id)->endNote(); } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_ui_fragments_PlayFragment_setMasterVolume(JNIEnv *env, jobject thiz, jdouble volume) { audioHost->masterVolume = volume; } -extern "C" JNIEXPORT void JNICALL Java_com_lukas_music_instruments_InternalInstrument_setInstrumentWaveform(JNIEnv *env, jobject thiz, jint id, jint waveform) { - Instrument *instrument = static_cast(listGet(audioHost->instruments->begin(), - id)); - instrument->setWaveform(static_cast(waveform)); + getInstrument(id)->setWaveform(static_cast(waveform)); +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_setVolume(JNIEnv *env, jobject thiz, jint id, + jfloat volume) { + getInstrument(id)->wave->amplitude = volume; +} + +JNIEXPORT void JNICALL +Java_com_lukas_music_instruments_InternalInstrument_destroy(JNIEnv *env, jobject thiz, jint id) { + listSet(audioHost->instruments->begin(), id, nullptr); + delete getInstrument(id); +} } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/EditVoiceFragment.kt b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt new file mode 100644 index 0000000..56f21e6 --- /dev/null +++ b/app/src/main/java/com/lukas/music/EditVoiceFragment.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2022 Lukas Eisenhauer + * + * This program 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, either version 3 of the License, or(at your option) any later version. + * + * This program 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 this program. If not, see . + */ + +package com.lukas.music + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TableRow +import androidx.core.view.setMargins +import androidx.fragment.app.DialogFragment +import com.google.android.material.button.MaterialButton +import com.lukas.music.databinding.FragmentEditVoiceBinding +import com.lukas.music.song.Song +import com.lukas.music.song.voice.Voice +import com.lukas.music.util.ArrayProperty +import com.lukas.music.util.setupToggle + +class EditVoiceFragment(private val voice: Voice) : DialogFragment() { + private lateinit var binding: FragmentEditVoiceBinding + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEditVoiceBinding.inflate(inflater) + for (row in 0 until voice.noteCount) { + val rowLayout = TableRow(binding.root.context) + for (column in 0 until Song.currentSong.beats) { + val button = MaterialButton(binding.root.context) + button.layoutParams = buttonLayout + button.setupToggle(ArrayProperty(voice.noteActive[column], row), R.color.blue) + rowLayout.addView(button) + } + binding.noteGrid.addView(rowLayout) + } + binding.noteGrid.isStretchAllColumns = true + binding.closeButton.setOnClickListener { + dismiss() + } + return binding.root + } + + override fun onStart() { + super.onStart() + dialog?.window?.setLayout( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + } + + companion object { + val buttonLayout = TableRow.LayoutParams(0, TableRow.LayoutParams.WRAP_CONTENT) + + init { + buttonLayout.setMargins(5) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/Instrument.kt b/app/src/main/java/com/lukas/music/instruments/Instrument.kt index 8ad90c6..92a896c 100644 --- a/app/src/main/java/com/lukas/music/instruments/Instrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/Instrument.kt @@ -10,42 +10,22 @@ package com.lukas.music.instruments -import com.lukas.music.databinding.FragmentInstrumentBinding import com.lukas.music.song.note.Note import com.lukas.music.song.voice.BassVoice -import com.lukas.music.song.voice.ChordVoice import com.lukas.music.song.voice.Voice -abstract class Instrument(private var name: String) { - private var active = false +abstract class Instrument(var name: String) { + var voice: Voice = BassVoice(this) abstract var waveform: Waveform - - fun applyToView(binding: FragmentInstrumentBinding) { - binding.instrumentNameText.text = name - binding.editInstrumentButton.setOnClickListener { - println("click instrument $name") - } - binding.activeSwitch.setOnCheckedChangeListener { _, newActive -> - active = newActive - changeActive(newActive) - } - binding.activeSwitch.isChecked = active - } + abstract var volume: Float + abstract var muted: Boolean abstract fun startNote(note: Note) abstract fun stop() - abstract fun changeActive(newActive: Boolean) + abstract fun stopNote(note: Note) + abstract fun destroy() companion object { - val instruments = - mutableListOf( - MonoInstrument("Bass"), - PolyInstrument("Chords"), - ) - - val voice = mutableListOf( - BassVoice(instruments[0]), - ChordVoice(instruments[1]), - ) + val instruments = mutableListOf() } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt index dc64bcc..b4c68a4 100644 --- a/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/InternalInstrument.kt @@ -10,33 +10,67 @@ package com.lukas.music.instruments +import com.lukas.music.song.note.Note + class InternalInstrument { private val id = createInstrument() - var active: Boolean = false - set(value) { - field = value - setInstrumentActive(id, value) - } + var note: Note? = null var waveform: Waveform = Waveform.SINE set(value) { field = value setInstrumentWaveform(id, value.id) - // this is to resend the setInstrumentActive for the new waveform in the internal c++ code - active = active + refresh() } - fun startNote(frequency: Double) { - startNote(id, frequency) + var volume: Float = 0.3f + set(value) { + field = value + if (!muted) { + actualVolume = value + } + } + + var muted: Boolean = false + set(value) { + field = value + actualVolume = if (value) 0.0f else volume + } + + private var actualVolume: Float = 1.0f + set(value) { + field = value + setVolume(id, value) + } + + init { + refresh() + } + + private fun refresh() { + // this is to resend the old information to the internal c++ code (when changing the waveform) + muted = muted + volume = volume + } + + fun startNote(note: Note) { + this.note = note + startNote(id, note.frequency) } fun endNote() { + note = null endNote(id) } + fun destroy() { + destroy(id) + } + private external fun createInstrument(): Int - private external fun setInstrumentActive(id: Int, isActive: Boolean) private external fun setInstrumentWaveform(id: Int, waveform: Int) private external fun startNote(id: Int, frequency: Double) private external fun endNote(id: Int) + private external fun setVolume(id: Int, volume: Float) + private external fun destroy(id: Int) } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt index e7b7464..2b43524 100644 --- a/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/MonoInstrument.kt @@ -21,15 +21,36 @@ internalInstrument.waveform = value } - override fun startNote(note: Note) { - internalInstrument.startNote(note.frequency) - } + override var volume: Float = 1.0f + set(value) { + field = value + internalInstrument.volume = volume + } - override fun changeActive(newActive: Boolean) { - internalInstrument.active = newActive + override var muted: Boolean = false + set(value) { + field = value + internalInstrument.muted = value + } + + override fun startNote(note: Note) { + if (note == internalInstrument.note) { + return + } + internalInstrument.startNote(note) } override fun stop() { internalInstrument.endNote() } + + override fun stopNote(note: Note) { + if (note == internalInstrument.note) { + stop() + } + } + + override fun destroy() { + internalInstrument.destroy() + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt index 9d8b4f9..26e4fbb 100644 --- a/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt +++ b/app/src/main/java/com/lukas/music/instruments/PolyInstrument.kt @@ -24,27 +24,55 @@ } } + override var volume: Float = 1.0f + set(value) { + field = value + for (internalInstrument in internalInstruments) { + internalInstrument.volume = volume + } + } + + override var muted: Boolean = false + set(value) { + field = value + for (instrument in internalInstruments) { + instrument.muted = value + } + } + override fun startNote(note: Note) { for ((index, instrumentPlaying) in playing.withIndex()) { if (!instrumentPlaying) { - internalInstruments[index].startNote(note.frequency) + internalInstruments[index].startNote(note) playing[index] = true return } + if (internalInstruments[index].note == note) { + return + } } throw IllegalStateException("cannot start another note with the current amount of oscillators") } - override fun changeActive(newActive: Boolean) { - for (instrument in internalInstruments) { - instrument.active = newActive - } - } - override fun stop() { for ((i, instrument) in internalInstruments.withIndex()) { instrument.endNote() playing[i] = false } } + + override fun stopNote(note: Note) { + for ((i, instrument) in internalInstruments.withIndex()) { + if (instrument.note == note) { + instrument.endNote() + playing[i] = false + } + } + } + + override fun destroy() { + for (instrument in internalInstruments) { + instrument.destroy() + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/Song.kt b/app/src/main/java/com/lukas/music/song/Song.kt index a3123f2..fc5421b 100644 --- a/app/src/main/java/com/lukas/music/song/Song.kt +++ b/app/src/main/java/com/lukas/music/song/Song.kt @@ -16,10 +16,33 @@ import com.lukas.music.util.Cycle class Song( - var root: Note, + root: Note, val beats: Int ) : Cycle(beats) { val chordProgression = ChordProgression() + var soloInstrument: Instrument? = null + set(value) { + field = value + value?.let { + for (instrument in Instrument.instruments) { + if (instrument != value) { + instrument.stop() + } + } + } + } + + var root: Note = root + set(value) { + field = value + stopAllInstruments() + } + + private fun stopAllInstruments() { + for (instrument in Instrument.instruments) { + instrument.stop() + } + } init { for (i in 0 until beats) { @@ -27,6 +50,7 @@ } wraparoundListeners += { chordProgression.step() + stopAllInstruments() } } @@ -34,8 +58,12 @@ super.step() val chord = chordProgression.currentItem?.currentItem ?: return index val chordNotes = chord.getNotes(root) - for (voice in Instrument.voice) { - voice.step(root, chordNotes) + soloInstrument?.let { + it.voice.step(root, chordNotes, index) + } ?: run { + for (instrument in Instrument.instruments) { + instrument.voice.step(root, chordNotes, index) + } } return index } diff --git a/app/src/main/java/com/lukas/music/song/chords/Chord.kt b/app/src/main/java/com/lukas/music/song/chords/Chord.kt index db45fdd..4400dae 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Chord.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Chord.kt @@ -19,6 +19,12 @@ interval = Interval(value) } var interval = Interval(note) + set(value) { + field = value + if (note != value.distance) { + note = value.distance + } + } fun getNotes(root: Note): Array { return Array(chordType.notes.size) { root + note + chordType.notes[it] } diff --git a/app/src/main/java/com/lukas/music/song/chords/Interval.kt b/app/src/main/java/com/lukas/music/song/chords/Interval.kt index 2726787..d112805 100644 --- a/app/src/main/java/com/lukas/music/song/chords/Interval.kt +++ b/app/src/main/java/com/lukas/music/song/chords/Interval.kt @@ -10,7 +10,7 @@ package com.lukas.music.song.chords -class Interval(private val distance: Int) { +class Interval(val distance: Int) { val name: IntervalName = when (distance) { 0 -> IntervalName.UNISON 1, 2 -> IntervalName.SECOND @@ -32,7 +32,7 @@ return name.toString() } - enum class IntervalName(val distance: Int, val romanVersion: String) { + enum class IntervalName(private val distance: Int, val romanVersion: String) { UNISON(0, "I"), SECOND(1, "II"), THIRD(3, "III"), diff --git a/app/src/main/java/com/lukas/music/song/note/Note.kt b/app/src/main/java/com/lukas/music/song/note/Note.kt index d3a588d..17bfb2b 100644 --- a/app/src/main/java/com/lukas/music/song/note/Note.kt +++ b/app/src/main/java/com/lukas/music/song/note/Note.kt @@ -28,6 +28,22 @@ return this + (-other) } + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Note + + if (id != other.id) return false + + return true + } + + override fun hashCode(): Int { + return id + } + + companion object { val NOTES = Array(128) { Note(it) } diff --git a/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt b/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt index 3990554..4706068 100644 --- a/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt +++ b/app/src/main/java/com/lukas/music/song/voice/BassVoice.kt @@ -14,9 +14,16 @@ import com.lukas.music.song.note.Note class BassVoice(instrument: Instrument) : Voice(instrument) { - override val steps = listOf(1, 3) + override var noteActive: Array> = arrayOf( + arrayOf(true), + arrayOf(false), + arrayOf(true), + arrayOf(false) + ) - override fun step(root: Note, chord: Array) { - instrument.startNote(chord[0] - 24) + override val noteCount: Int = 1 + + override fun getNotes(root: Note, chordNotes: Array): Array { + return arrayOf(chordNotes[0] - 24) } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/voice/ChordVoice.kt b/app/src/main/java/com/lukas/music/song/voice/ChordVoice.kt index fc5362d..ab7117f 100644 --- a/app/src/main/java/com/lukas/music/song/voice/ChordVoice.kt +++ b/app/src/main/java/com/lukas/music/song/voice/ChordVoice.kt @@ -14,12 +14,15 @@ import com.lukas.music.song.note.Note class ChordVoice(instrument: Instrument) : Voice(instrument) { - override val steps: List = listOf(2, 4) + override var noteActive: Array> = arrayOf( + Array(3) { false }, + Array(3) { true }, + Array(3) { false }, + Array(3) { true }, + ) + override val noteCount: Int = 3 - override fun step(root: Note, chord: Array) { - instrument.stop() - for (note in chord) { - instrument.startNote(note) - } + override fun getNotes(root: Note, chordNotes: Array): Array { + return chordNotes } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/song/voice/Voice.kt b/app/src/main/java/com/lukas/music/song/voice/Voice.kt index f7fca94..4f56c2a 100644 --- a/app/src/main/java/com/lukas/music/song/voice/Voice.kt +++ b/app/src/main/java/com/lukas/music/song/voice/Voice.kt @@ -12,9 +12,36 @@ import com.lukas.music.instruments.Instrument import com.lukas.music.song.note.Note +import kotlin.reflect.KClass abstract class Voice(val instrument: Instrument) { - abstract val steps: List + abstract var noteActive: Array> + abstract val noteCount: Int - abstract fun step(root: Note, chord: Array) + abstract fun getNotes(root: Note, chordNotes: Array): Array + + fun step(root: Note, chordNotes: Array, beat: Int) { + if (instrument.muted) { + return + } + val activeNotes = noteActive[beat] + val notes = getNotes(root, chordNotes) + for ((index, active) in activeNotes.withIndex()) { + val note = notes[index] + if (!active) { + instrument.stopNote(note) + continue + } + instrument.startNote(note) + } + } + + companion object { + val DEFAULT_VOICES = listOf>( + BassVoice::class, + ChordVoice::class, + ) + + val DEFAULT_VOICE_NAMES = listOf("Bass", "Chord") + } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/adapters/InstrumentAdapter.kt b/app/src/main/java/com/lukas/music/ui/adapters/InstrumentAdapter.kt index 8961831..d51f62e 100644 --- a/app/src/main/java/com/lukas/music/ui/adapters/InstrumentAdapter.kt +++ b/app/src/main/java/com/lukas/music/ui/adapters/InstrumentAdapter.kt @@ -11,54 +11,26 @@ package com.lukas.music.ui.adapters import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup -import android.widget.AdapterView -import android.widget.ArrayAdapter import androidx.recyclerview.widget.RecyclerView import com.lukas.music.databinding.FragmentInstrumentBinding import com.lukas.music.instruments.Instrument -import com.lukas.music.instruments.Waveform +import com.lukas.music.ui.fragments.InstrumentListFragment -class InstrumentAdapter : RecyclerView.Adapter() { - class InstrumentViewHolder(val binding: FragmentInstrumentBinding) : - RecyclerView.ViewHolder(binding.root), AdapterView.OnItemSelectedListener { - lateinit var instrument: Instrument - - init { - val adapter = ArrayAdapter( - binding.root.context, - android.R.layout.simple_spinner_dropdown_item, Waveform.VALUES - ) - binding.waveformSelection.adapter = adapter - binding.waveformSelection.onItemSelectedListener = this - } - - override fun onItemSelected( - adapterView: AdapterView<*>?, - view: View?, - position: Int, - id: Long - ) { - instrument.waveform = Waveform.VALUES[position] - } - - override fun onNothingSelected(adapterView: AdapterView<*>?) { - } - } +class InstrumentAdapter(val parent: InstrumentListFragment) : + RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): InstrumentViewHolder { val context = parent.context val inflater = LayoutInflater.from(context) val binding = FragmentInstrumentBinding.inflate(inflater, parent, false) - return InstrumentViewHolder(binding) + return InstrumentViewHolder(binding, this.parent.childFragmentManager, this) } override fun onBindViewHolder(holder: InstrumentViewHolder, position: Int) { val instrument = Instrument.instruments[position] holder.instrument = instrument - instrument.applyToView(holder.binding) } override fun getItemCount(): Int { diff --git a/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt b/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt new file mode 100644 index 0000000..3e129bc --- /dev/null +++ b/app/src/main/java/com/lukas/music/ui/adapters/InstrumentViewHolder.kt @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2022 Lukas Eisenhauer + * + * This program 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, either version 3 of the License, or(at your option) any later version. + * + * This program 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 this program. If not, see . + */ + +package com.lukas.music.ui.adapters + +import androidx.fragment.app.FragmentManager +import androidx.recyclerview.widget.RecyclerView +import com.lukas.music.R +import com.lukas.music.databinding.FragmentInstrumentBinding +import com.lukas.music.instruments.Instrument +import com.lukas.music.song.Song +import com.lukas.music.ui.fragments.EditInstrumentFragment +import com.lukas.music.util.setupToggle +import com.lukas.music.util.updateToggle + +class InstrumentViewHolder( + val binding: FragmentInstrumentBinding, + private val childFragmentManager: FragmentManager, + private val adapter: InstrumentAdapter +) : + RecyclerView.ViewHolder(binding.root) { + init { + HOLDERS += this + } + + var solo: Boolean = false + set(value) { + if (value) { + for (holder in HOLDERS) { + holder.solo = false + } + Song.currentSong.soloInstrument = instrument + } + field = value + binding.soloButton.updateToggle(this::solo, R.color.blue) + } + + var instrument: Instrument? = null + set(value) { + field = value + value ?: return + binding.instrumentNameText.text = instrument?.name + binding.editInstrumentButton.setOnClickListener { + EditInstrumentFragment(instrument!!, this).showNow(childFragmentManager, "") + } + binding.muteButton.setupToggle(instrument!!::muted, R.color.red) + binding.soloButton.setupToggle(this::solo, R.color.blue) { + if (!it) { + Song.currentSong.soloInstrument = null + } + } + binding.deleteButton.setOnClickListener { + val index = Instrument.instruments.indexOf(instrument) + Instrument.instruments -= instrument!! + adapter.notifyItemRemoved(index) + instrument!!.destroy() + } + } + + companion object { + val HOLDERS = mutableListOf() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/fragments/EditChordFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/EditChordFragment.kt index a311a6c..bd7a3d9 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/EditChordFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/EditChordFragment.kt @@ -10,13 +10,10 @@ package com.lukas.music.ui.fragments -import android.R import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.AdapterView -import android.widget.ArrayAdapter import androidx.fragment.app.DialogFragment import com.lukas.music.databinding.FragmentEditChordBinding import com.lukas.music.song.Scale @@ -24,8 +21,10 @@ import com.lukas.music.song.chords.Chord import com.lukas.music.song.chords.ChordType import com.lukas.music.song.chords.Interval +import com.lukas.music.util.setup -class EditChordFragment(val chord: Chord, val songFragment: SongFragment) : DialogFragment() { +class EditChordFragment(val chord: Chord, private val songFragment: SongFragment) : + DialogFragment() { lateinit var binding: FragmentEditChordBinding override fun onCreateView( @@ -45,24 +44,12 @@ val pitches = if (songFragment.displayChordNames) { Array(Scale.MAJOR.steps.size) { (Song.currentSong.root + Scale.MAJOR.steps[it]).noteName.toString() } } else Interval.IntervalName.NAMES - val pitchAdapter = ArrayAdapter( - binding.root.context, - R.layout.simple_spinner_dropdown_item, pitches - ) - binding.pitchSpinner.adapter = pitchAdapter - binding.pitchSpinner.setSelection(chord.interval.name.ordinal) - binding.pitchSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { - override fun onItemSelected( - parent: AdapterView<*>?, - view: View?, - position: Int, - id: Long - ) { - chord.note = Scale.MAJOR.steps[position] - songFragment.updateChords() + binding.pitchSpinner.setup(pitches, chord.interval.name.ordinal) { + chord.note = Scale.MAJOR.steps[it] + if (binding.typeSpinner.selectedItemPosition == 0) { + chord.chordType = Scale.MAJOR.chordTypes[chord.interval.name.ordinal] } - - override fun onNothingSelected(parent: AdapterView<*>?) {} + songFragment.updateChords() } } @@ -71,32 +58,17 @@ for (chordType in ChordType.VALUES) { values += chordType.toString() } - val modifierAdapter = ArrayAdapter( - binding.root.context, - R.layout.simple_spinner_dropdown_item, values - ) - binding.typeSpinner.adapter = modifierAdapter - binding.typeSpinner.setSelection( - if (chord.chordType == Scale.MAJOR.chordTypes[chord.interval.name.ordinal]) - 0 + binding.typeSpinner.setup( + values, + if (chord.chordType == Scale.MAJOR.chordTypes[chord.interval.name.ordinal]) 0 else chord.chordType.ordinal + 1 - ) - binding.typeSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { - override fun onItemSelected( - parent: AdapterView<*>?, - view: View?, - position: Int, - id: Long - ) { - if (position == 0) { - chord.chordType = Scale.MAJOR.chordTypes[chord.interval.name.ordinal] - } else { - chord.chordType = ChordType.VALUES[position - 1] - } - songFragment.updateChords() + ) { + if (it == 0) { + chord.chordType = Scale.MAJOR.chordTypes[chord.interval.name.ordinal] + } else { + chord.chordType = ChordType.VALUES[it - 1] } - - override fun onNothingSelected(parent: AdapterView<*>?) {} + songFragment.updateChords() } } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/fragments/EditInstrumentFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/EditInstrumentFragment.kt new file mode 100644 index 0000000..0020ae8 --- /dev/null +++ b/app/src/main/java/com/lukas/music/ui/fragments/EditInstrumentFragment.kt @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2022 Lukas Eisenhauer + * + * This program 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, either version 3 of the License, or(at your option) any later version. + * + * This program 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 this program. If not, see . + */ + +package com.lukas.music.ui.fragments + +import android.os.Bundle +import android.text.Editable +import android.text.TextWatcher +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.DialogFragment +import com.lukas.music.EditVoiceFragment +import com.lukas.music.databinding.FragmentEditInstrumentBinding +import com.lukas.music.instruments.Instrument +import com.lukas.music.instruments.Waveform +import com.lukas.music.song.voice.Voice +import com.lukas.music.ui.adapters.InstrumentViewHolder +import com.lukas.music.util.setup +import com.lukas.music.util.smartSetup + +class EditInstrumentFragment( + private val instrument: Instrument, + private val viewHolder: InstrumentViewHolder +) : DialogFragment() { + lateinit var binding: FragmentEditInstrumentBinding + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentEditInstrumentBinding.inflate(inflater) + binding.instrumentNameTextBox.text.clear() + binding.instrumentNameTextBox.text.append(instrument.name) + binding.instrumentNameTextBox.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} + + override fun afterTextChanged(s: Editable?) { + instrument.name = binding.instrumentNameTextBox.text.toString() + viewHolder.instrument = viewHolder.instrument + } + }) + binding.waveformSelection.smartSetup(Waveform.VALUES, instrument::waveform) + binding.volumeSeek.setup(0, 100, 30) { + binding.volumeText.text = "volume: $it%" + instrument.volume = it.toFloat() / 100f + } + binding.voiceSelection.setup( + Voice.DEFAULT_VOICE_NAMES, + Voice.DEFAULT_VOICES.indexOf(instrument.voice::class) + ) { + if (instrument.voice::class == Voice.DEFAULT_VOICES[it]) { + return@setup + } + instrument.voice = Voice.DEFAULT_VOICES[it].constructors.first().call(instrument) + } + binding.editVoiceButton.setOnClickListener { + EditVoiceFragment(instrument.voice).showNow(childFragmentManager, "") + } + binding.closeButton.setOnClickListener { + dismiss() + } + return binding.root + } + + override fun onStart() { + super.onStart() + dialog?.window?.setLayout( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/fragments/InstrumentListFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/InstrumentListFragment.kt index 3f1a910..1d35c10 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/InstrumentListFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/InstrumentListFragment.kt @@ -10,13 +10,19 @@ package com.lukas.music.ui.fragments +import android.app.AlertDialog import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment +import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView import com.lukas.music.databinding.FragmentInstrumentListBinding +import com.lukas.music.instruments.Instrument +import com.lukas.music.instruments.MonoInstrument +import com.lukas.music.instruments.PolyInstrument import com.lukas.music.ui.adapters.InstrumentAdapter class InstrumentListFragment : Fragment() { @@ -27,8 +33,53 @@ savedInstanceState: Bundle? ): View? { binding = FragmentInstrumentListBinding.inflate(inflater) - binding.recyclerView.adapter = InstrumentAdapter() + binding.recyclerView.adapter = InstrumentAdapter(this) binding.recyclerView.layoutManager = LinearLayoutManager(context) + val callback = object : ItemTouchHelper.SimpleCallback( + ItemTouchHelper.UP or ItemTouchHelper.DOWN, + 0 + ) { + override fun onMove( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder + ): Boolean { + val adapter = recyclerView.adapter as InstrumentAdapter + val startPosition = viewHolder.adapterPosition + val endPosition = target.adapterPosition + val instrument = Instrument.instruments[startPosition] + Instrument.instruments.removeAt(startPosition) + if (endPosition < startPosition) { + Instrument.instruments.add(endPosition + 1, instrument) + } else { + Instrument.instruments.add(endPosition - 1, instrument) + } + adapter.notifyItemMoved(startPosition, endPosition) + return true + } + + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {} + } + val helper = ItemTouchHelper(callback) + helper.attachToRecyclerView(binding.recyclerView) + + val builder = AlertDialog.Builder(binding.root.context) + builder.setTitle("Instrument type:") + .setItems( + arrayOf("mono", "poly") + ) { _, index -> + when (index) { + 0 -> Instrument.instruments += MonoInstrument("New mono Instrument") + 1 -> Instrument.instruments += PolyInstrument("New poly Instrument") + } + (binding.recyclerView.adapter as RecyclerView.Adapter).notifyItemInserted( + Instrument.instruments.size - 1 + ) + } + builder.create() + binding.addInstrumentButton.setOnClickListener { + builder.show() + } return binding.root } } \ No newline at end of file diff --git a/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt b/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt index eaf2c61..0c33019 100644 --- a/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt +++ b/app/src/main/java/com/lukas/music/ui/fragments/PlayFragment.kt @@ -16,7 +16,10 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.* +import android.widget.RadioButton +import android.widget.RadioGroup +import android.widget.Space +import android.widget.TextView import androidx.cardview.widget.CardView import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment @@ -24,6 +27,7 @@ import com.lukas.music.databinding.FragmentPlayBinding import com.lukas.music.instruments.Rhythm import com.lukas.music.song.Song +import com.lukas.music.util.setup class PlayFragment : Fragment() { private lateinit var binding: FragmentPlayBinding @@ -58,11 +62,11 @@ } Song.currentSong.chordProgression.reverse() } - setupSlider(binding.masterVolumeSlider, 0, 100, 100) { + binding.masterVolumeSlider.setup(0, 100, 100) { setMasterVolume(it.toDouble() / 100.0) binding.masterVolumeText.text = "Master volume: $it%" } - setupSlider(binding.tempoSlider, 50, 150, 90) { + binding.tempoSlider.setup(50, 150, 90) { Rhythm.setTempo(it) binding.tempoText.text = "tempo: ${it}bpm" } @@ -116,33 +120,6 @@ ) } - private fun setupSlider( - slider: SeekBar, - min: Int, - max: Int, - initialProgress: Int, - callback: (Int) -> Unit - ) { - slider.min = min - slider.max = max - slider.setOnSeekBarChangeListener(object : - SeekBar.OnSeekBarChangeListener { - override fun onProgressChanged( - seekBar: SeekBar, - progress: Int, fromUser: Boolean - ) { - callback(progress) - } - - override fun onStartTrackingTouch(seekBar: SeekBar) { - } - - override fun onStopTrackingTouch(seekBar: SeekBar) { - } - }) - slider.progress = initialProgress - } - fun updateChords() { binding.phraseDisplay.removeAllViews() chordDisplays.clear() diff --git a/app/src/main/java/com/lukas/music/util/ArrayProperty.kt b/app/src/main/java/com/lukas/music/util/ArrayProperty.kt new file mode 100644 index 0000000..938f07c --- /dev/null +++ b/app/src/main/java/com/lukas/music/util/ArrayProperty.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2022 Lukas Eisenhauer + * + * This program 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, either version 3 of the License, or(at your option) any later version. + * + * This program 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 this program. If not, see . + */ + +package com.lukas.music.util + +import kotlin.reflect.* + +class ArrayProperty(private val array: Array, private val position: Int) : + KMutableProperty0 { + override val annotations: List = listOf() + override val isAbstract: Boolean = false + override val isFinal: Boolean = false + override val isOpen: Boolean = false + override val isSuspend: Boolean = false + override val name: String = "TODO" + override val parameters: List = listOf() + override val returnType: KType get() = TODO() + override val typeParameters: List = listOf() + override val visibility: KVisibility? = KVisibility.PUBLIC + override fun call(vararg args: Any?): T = TODO() + override fun callBy(args: Map): T = TODO() + override val setter: KMutableProperty0.Setter get() = TODO() + + + override val isConst: Boolean = false + override val isLateinit: Boolean = false + override val getter: KProperty0.Getter get() = TODO() + + + override fun getDelegate(): Any? = null + override fun invoke(): T = TODO() + + override fun set(value: T) { + array[position] = value + } + + + override fun get(): T = array[position] +} diff --git a/app/src/main/java/com/lukas/music/util/UIUtil.kt b/app/src/main/java/com/lukas/music/util/UIUtil.kt new file mode 100644 index 0000000..da897c5 --- /dev/null +++ b/app/src/main/java/com/lukas/music/util/UIUtil.kt @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2022 Lukas Eisenhauer + * + * This program 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, either version 3 of the License, or(at your option) any later version. + * + * This program 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 this program. If not, see . + */ + +package com.lukas.music.util + +import android.view.View +import android.widget.* +import androidx.core.content.ContextCompat +import com.lukas.music.R +import kotlin.reflect.KMutableProperty0 + +fun SeekBar.setup( + min: Int, max: Int, initialProgress: Int, callback: (Int) -> Unit +) { + this.min = min + this.max = max + setOnSeekBarChangeListener(object : + SeekBar.OnSeekBarChangeListener { + override fun onProgressChanged( + seekBar: SeekBar, + progress: Int, fromUser: Boolean + ) { + callback(progress) + } + + override fun onStartTrackingTouch(seekBar: SeekBar) { + } + + override fun onStopTrackingTouch(seekBar: SeekBar) { + } + }) + this.progress = initialProgress +} + +fun Button.setupToggle( + target: KMutableProperty0, + activeColor: Int, + callback: (Boolean) -> Unit = {} +) { + setOnClickListener { + target.set(!target.get()) + updateToggle(target, activeColor) + callback(target.get()) + } + updateToggle(target, activeColor) +} + +fun Button.updateToggle(target: KMutableProperty0, activeColor: Int) { + setBackgroundColor( + ContextCompat.getColor(context, if (target.get()) activeColor else R.color.gray_0x60) + ) +} + +fun Spinner.setup( + items: Array, + initialIndex: Int, + callback: (Int) -> Unit = {}, +) { + val arrayAdapter = ArrayAdapter( + context, + android.R.layout.simple_spinner_dropdown_item, items + ) + spinnerSetupMain(arrayAdapter, initialIndex, callback) +} + +fun Spinner.setup( + items: List, + initialIndex: Int, + callback: (Int) -> Unit = {}, +) { + val arrayAdapter = ArrayAdapter( + context, + android.R.layout.simple_spinner_dropdown_item, items + ) + spinnerSetupMain(arrayAdapter, initialIndex, callback) +} + +private fun Spinner.spinnerSetupMain( + arrayAdapter: ArrayAdapter, + initialIndex: Int, + callback: (Int) -> Unit +) { + adapter = arrayAdapter + setSelection(initialIndex) + onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onItemSelected( + parent: AdapterView<*>?, + view: View?, + position: Int, + id: Long + ) { + callback(position) + } + + override fun onNothingSelected(parent: AdapterView<*>?) {} + } +} + +fun > Spinner.smartSetup( + items: Array, + target: KMutableProperty0, + callback: (Int) -> Unit = {} +) { + val arrayAdapter = ArrayAdapter( + context, + android.R.layout.simple_spinner_dropdown_item, items + ) + spinnerSetupMain(arrayAdapter, target.get().ordinal) { + if (target.get() != items[it]) { + target.set(items[it]) + } + callback(it) + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_edit_instrument.xml b/app/src/main/res/layout/fragment_edit_instrument.xml new file mode 100644 index 0000000..b5578fc --- /dev/null +++ b/app/src/main/res/layout/fragment_edit_instrument.xml @@ -0,0 +1,151 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_edit_voice.xml b/app/src/main/res/layout/fragment_edit_voice.xml new file mode 100644 index 0000000..b29b2c7 --- /dev/null +++ b/app/src/main/res/layout/fragment_edit_voice.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_instrument.xml b/app/src/main/res/layout/fragment_instrument.xml index 5377212..c0d534a 100644 --- a/app/src/main/res/layout/fragment_instrument.xml +++ b/app/src/main/res/layout/fragment_instrument.xml @@ -29,6 +29,7 @@ android:layout_marginTop="20dp" android:layout_marginEnd="20dp" android:text="@string/placeholder" + android:textSize="16sp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> @@ -44,35 +45,46 @@ android:cropToPadding="true" android:keepScreenOn="true" android:src="@android:drawable/ic_menu_edit" - app:backgroundTint="#1565C0" + app:backgroundTint="@color/material_dynamic_primary50" app:fabSize="mini" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/instrumentNameText" /> - + +